diff --git a/README.md b/README.md index 3b0c037b..2c72a1f6 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Try out all the unique features using the CameraKit Demo from the Google Play st - [`ckFlash`](#ckflash) - [`ckPictureMode`](#ckpicturemode) - [`ckTapToFocus`](#cktaptofocus) + - [`ckAutoFocus`](#ckautofocus) - [Capturing Images](#capturing-images) - [Capturing Video](#capturing-video) - [Automatic Permissions Behavior](#automatic-permissions-behavior) @@ -124,7 +125,8 @@ camera.setCameraListener(new CameraListener() { camerakit:ckFacing="back" camerakit:ckFlash="off" camerakit:ckPictureMode="quality" - camerakit:ckTapToFocus="on" /> + camerakit:ckTapToFocus="on" + camerakit:ckAutoFocus="true" /> ``` #### `ckCropOutput` @@ -134,7 +136,6 @@ camera.setCameraListener(new CameraListener() { | `true` | Crop the output image or video to only contain what can be seen on the `CameraView` preview. | | `false` | Output the full image or video regardless of what is visible on the `CameraView` preview. | - #### `ckFacing` | Value | Description | @@ -167,6 +168,13 @@ camera.setCameraListener(new CameraListener() { | `invisible` | Tap to focus is enabled, but no focus circle appears. | | `off` | Tap to focus is off. | +#### `ckAutoFocus` + +| Value | Description | +| --------------| -------------| +| `true` | Continuously allow the `CameraView` preview to adjust focus automatically. | +| `false` | Never adjust focus during preview. | + ### Capturing Images To capture an image just call `CameraView.capturePicture()`. Make sure you setup a `CameraListener` to handle the image callback. diff --git a/build.gradle b/build.gradle index b1557280..a3330d48 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0-beta1' + classpath 'com.android.tools.build:gradle:2.2.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/camerakit/src/main/AndroidManifest.xml b/camerakit/src/main/AndroidManifest.xml index 1bbbb482..f108e9a6 100644 --- a/camerakit/src/main/AndroidManifest.xml +++ b/camerakit/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ package="com.flurgle.camerakit"> + FLASH_MODES = new SparseArrayCompat<>(); + + static { + FLASH_MODES.put(CameraKit.Constants.FLASH_OFF, Camera.Parameters.FLASH_MODE_OFF); + FLASH_MODES.put(CameraKit.Constants.FLASH_ON, Camera.Parameters.FLASH_MODE_ON); + FLASH_MODES.put(CameraKit.Constants.FLASH_AUTO, Camera.Parameters.FLASH_MODE_AUTO); + } + + private int mCameraId; + + Camera mCamera; + + private Camera.Parameters mCameraParameters; + + private final Camera.CameraInfo mCameraInfo = new Camera.CameraInfo(); + + private boolean mShowingPreview; + + private boolean mAutoFocus; + + private int mFacing; + + private int mFlash; + + private int mDisplayOrientation; + + private SortedSet mPreviewSizes; + private SortedSet mCaptureSizes; + + private MediaRecorder mMediaRecorder; + + Camera1(CameraListener callback, PreviewImpl preview) { + super(callback, preview); + preview.setCallback(new PreviewImpl.Callback() { + @Override + public void onSurfaceChanged() { + if (mCamera != null) { + setUpPreview(); + adjustCameraParameters(); + } + } + }); + + + mPreviewSizes = new TreeSet<>(); + mCaptureSizes = new TreeSet<>(); + } + + @Override + void start() { + chooseCamera(); + openCamera(); + if (mPreview.isReady()) { + setUpPreview(); + } + mShowingPreview = true; + mCamera.startPreview(); + } + + @Override + void stop() { + if (mCamera != null) { + mCamera.stopPreview(); + } + mShowingPreview = false; + releaseCamera(); + } + + @Override + boolean isCameraOpened() { + return mCamera != null; + } + + @Override + void setFacing(int facing) { + if (mFacing == facing) { + return; + } + mFacing = facing; + if (isCameraOpened()) { + stop(); + start(); + } + } + + @Override + int getFacing() { + return mFacing; + } + + @Override + void setFlash(int flash) { + if (flash == mFlash) { + return; + } + if (setFlashInternal(flash)) { + mCamera.setParameters(mCameraParameters); + } + } + + @Override + int getFlash() { + return mFlash; + } + + @Override + boolean getAutoFocus() { + if (!isCameraOpened()) { + return mAutoFocus; + } + String focusMode = mCameraParameters.getFocusMode(); + return focusMode != null && focusMode.contains("continuous"); + } + + @Override + void capturePicture() { + if (!isCameraOpened()) { + throw new IllegalStateException("Camera is not ready. Call start() before takePicture()."); + } + if (getAutoFocus()) { + mCamera.cancelAutoFocus(); + mCamera.autoFocus(new Camera.AutoFocusCallback() { + @Override + public void onAutoFocus(boolean success, Camera camera) { + takePictureInternal(); + } + }); + } else { + takePictureInternal(); + } + } + + void takePictureInternal() { + mCamera.takePicture(null, null, null, new Camera.PictureCallback() { + @Override + public void onPictureTaken(byte[] data, Camera camera) { + getCameraListener().onPictureTaken(data); + camera.startPreview(); + } + }); + } + + @Override + void captureStill() { + + } + + @Override + void startVideo() { + try { + prepareMediaRecorder(); + } catch (IOException e) { + if (mMediaRecorder != null) { + mMediaRecorder.release(); + } + + return; + } + } + + void prepareMediaRecorder() throws IOException { + mCamera.unlock(); + + mMediaRecorder = new MediaRecorder(); + mMediaRecorder.setCamera(mCamera); + + mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); + mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); + + mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)); + + mMediaRecorder.setOutputFile(new File(getView().getContext().getExternalFilesDir(null), "video.mp4").getAbsolutePath()); + + mMediaRecorder.prepare(); + mMediaRecorder.start(); + } + + @Override + void endVideo() { + if (mMediaRecorder != null) { + mMediaRecorder.stop(); + } + + getCameraListener().onVideoTaken(new File(getView().getContext().getExternalFilesDir(null), "video.mp4")); + } + + @Override + void setDisplayOrientation(int displayOrientation) { + if (mDisplayOrientation == displayOrientation) { + return; + } + mDisplayOrientation = displayOrientation; + if (isCameraOpened()) { + int cameraRotation = calcCameraRotation(displayOrientation); + mCameraParameters.setRotation(cameraRotation); + mCamera.setParameters(mCameraParameters); + final boolean needsToStopPreview = mShowingPreview && Build.VERSION.SDK_INT < 14; + if (needsToStopPreview) { + mCamera.stopPreview(); + } + mCamera.setDisplayOrientation(cameraRotation); + if (needsToStopPreview) { + mCamera.startPreview(); + } + } + } + + private int calcCameraRotation(int rotation) { + if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + return (360 - (mCameraInfo.orientation + rotation) % 360) % 360; + } else { // back-facing + return (mCameraInfo.orientation - rotation + 360) % 360; + } + } + + private void setUpPreview() { + try { + if (mPreview.getOutputClass() == SurfaceHolder.class) { + final boolean needsToStopPreview = mShowingPreview && Build.VERSION.SDK_INT < 14; + if (needsToStopPreview) { + mCamera.stopPreview(); + } + mCamera.setPreviewDisplay(mPreview.getSurfaceHolder()); + if (needsToStopPreview) { + mCamera.startPreview(); + } + } else { + mCamera.setPreviewTexture(mPreview.getSurfaceTexture()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void adjustCameraParameters() { + SortedSet sizes = mPreviewSizes; + Size previewSize = chooseOptimalSize(sizes); + final Camera.Size currentSize = mCameraParameters.getPictureSize(); + if (currentSize.width != previewSize.getWidth() || currentSize.height != previewSize.getHeight()) { + final Size pictureSize = mCaptureSizes.last(); + if (mShowingPreview) { + mCamera.stopPreview(); + } + + mCameraParameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight()); + mPreview.setTruePreviewSize(previewSize.getWidth(), previewSize.getHeight()); + + mCameraParameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); + mCameraParameters.setRotation(calcCameraRotation(mDisplayOrientation)); + setAutoFocusInternal(mAutoFocus); + setFlashInternal(mFlash); + mCamera.setParameters(mCameraParameters); + if (mShowingPreview) { + mCamera.startPreview(); + } + } + } + + private Size chooseOptimalSize(SortedSet sizes) { + if (!mPreview.isReady()) { // Not yet laid out + return sizes.last(); // Return the smallest size + } + + int desiredWidth; + int desiredHeight; + final int surfaceWidth = mPreview.getWidth(); + final int surfaceHeight = mPreview.getHeight(); + if (mDisplayOrientation == 90 || mDisplayOrientation == 270) { + desiredWidth = surfaceHeight; + desiredHeight = surfaceWidth; + } else { + desiredWidth = surfaceWidth; + desiredHeight = surfaceHeight; + } + Size result = null; + for (Size size : sizes) { // Iterate from small to large + if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) { + return size; + + } + result = size; + } + return result; + } + + private void chooseCamera() { + for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { + Camera.getCameraInfo(i, mCameraInfo); + if (mCameraInfo.facing == mFacing) { + mCameraId = i; + return; + } + } + mCameraId = INVALID_CAMERA_ID; + } + + private void openCamera() { + if (mCamera != null) { + releaseCamera(); + } + mCamera = Camera.open(mCameraId); + mCameraParameters = mCamera.getParameters(); + // Supported preview sizes + mPreviewSizes.clear(); + for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) { + mPreviewSizes.add(new Size(size.width, size.height)); + } + // Supported picture sizes; + mCaptureSizes.clear(); + for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) { + mCaptureSizes.add(new Size(size.width, size.height)); + } + + adjustCameraParameters(); + mCamera.setDisplayOrientation(calcCameraRotation(mDisplayOrientation)); + + getCameraListener().onCameraOpened(); + } + + private void releaseCamera() { + if (mCamera != null) { + mCamera.release(); + mCamera = null; + getCameraListener().onCameraClosed(); + } + } + + private boolean setAutoFocusInternal(boolean autoFocus) { + mAutoFocus = autoFocus; + if (isCameraOpened()) { + final List modes = mCameraParameters.getSupportedFocusModes(); + if (autoFocus && modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { + mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); + } else if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) { + mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED); + } else if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) { + mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY); + } else { + mCameraParameters.setFocusMode(modes.get(0)); + } + return true; + } else { + return false; + } + } + + private boolean setFlashInternal(int flash) { + if (isCameraOpened()) { + List modes = mCameraParameters.getSupportedFlashModes(); + String mode = FLASH_MODES.get(flash); + if (modes != null && modes.contains(mode)) { + mCameraParameters.setFlashMode(mode); + mFlash = flash; + return true; + } + String currentMode = FLASH_MODES.get(mFlash); + if (modes == null || !modes.contains(currentMode)) { + mCameraParameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); + mFlash = CameraKit.Constants.FLASH_OFF; + return true; + } + return false; + } else { + mFlash = flash; + return false; + } + } + +} diff --git a/camerakit/src/main/java/com/flurgle/camerakit/Camera2.java b/camerakit/src/main/java/com/flurgle/camerakit/Camera2.java index 0f01b6c4..a428a0ca 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/Camera2.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/Camera2.java @@ -3,6 +3,7 @@ package com.flurgle.camerakit; import android.annotation.TargetApi; import android.content.Context; import android.graphics.ImageFormat; +import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; @@ -13,7 +14,6 @@ import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.Image; import android.media.ImageReader; -import android.media.MediaRecorder; import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.NonNull; @@ -22,6 +22,12 @@ import android.util.SparseIntArray; import android.view.Surface; import android.view.View; +import com.flurgle.camerakit.encoding.VideoEncoder; +import com.flurgle.camerakit.utils.AspectRatio; +import com.flurgle.camerakit.utils.Size; +import com.flurgle.camerakit.utils.YuvUtils; + +import java.io.IOException; import java.util.Arrays; import java.util.SortedSet; import java.util.TreeSet; @@ -45,7 +51,7 @@ public class Camera2 extends CameraViewImpl { private CameraCaptureSession mCaptureSession; private CaptureRequest.Builder mPreviewRequestBuilder; private ImageReader mImageReader; - private MediaRecorder mMediaRecorder; + private VideoEncoder mVideoEncoder; private Semaphore mCameraOpenCloseLock; @@ -61,6 +67,10 @@ public class Camera2 extends CameraViewImpl { private HandlerThread mBackgroundThread; private Handler mBackgroundHandler; + private boolean mCropOutput; + + private boolean mIsRecording; + Camera2(Context context, CameraListener cameraListener, PreviewImpl preview) { super(cameraListener, preview); mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); @@ -163,6 +173,11 @@ public class Camera2 extends CameraViewImpl { return mFlash; } + @Override + boolean getAutoFocus() { + return true; + } + @Override void capturePicture() { if (mFacing == INTERNAL_FACINGS.get(CameraKit.Constants.FACING_BACK)) { @@ -184,11 +199,17 @@ public class Camera2 extends CameraViewImpl { if (mCamera == null) { return; } + + mIsRecording = true; } @Override void endVideo() { - + mIsRecording = false; + if (mVideoEncoder != null) { + mVideoEncoder.stopEncoder(); + mVideoEncoder = null; + } } @Override @@ -214,6 +235,10 @@ public class Camera2 extends CameraViewImpl { } } + void setCropOutput(boolean cropOutput) { + this.mCropOutput = cropOutput; + } + private boolean chooseCameraIdByFacing() { try { int internalFacing = mFacing; @@ -373,7 +398,7 @@ public class Camera2 extends CameraViewImpl { Size previewSize = getOptimalPreviewSize(); - mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight()); + mPreview.setTruePreviewSize(previewSize.getWidth(), previewSize.getHeight()); Surface surface = mPreview.getSurface(); try { mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); @@ -412,6 +437,8 @@ public class Camera2 extends CameraViewImpl { 360) % 360); // Stop preview and capture a still picture. mCaptureSession.stopRepeating(); + + mOnImageAvailableListener.allowCallback(); mCaptureSession.capture(captureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { @@ -437,7 +464,6 @@ public class Camera2 extends CameraViewImpl { } } - private final CameraDevice.StateCallback mCameraDeviceCallback = new CameraDevice.StateCallback() { @Override @@ -545,15 +571,41 @@ public class Camera2 extends CameraViewImpl { return; } - if (!mAllowOneCallback) { + if (!mAllowOneCallback && !mIsRecording) { image.close(); return; } - mAllowOneCallback = false; + Rect crop = null; + if (mCropOutput) { + + } + + if (mAllowOneCallback) { + mAllowOneCallback = false; + byte[] out = YuvUtils.createRGB(image, crop); + getCameraListener().onPictureTaken(out); + } + + if (mIsRecording) { + if (mVideoEncoder == null) { + try { + mVideoEncoder = new VideoEncoder(getView().getContext(), mFacing, image.getWidth(), image.getHeight()); + } catch (IOException e) { + + } + } + + if (mVideoEncoder != null) { + byte[] out = YuvUtils.getYUVData(image); + try { + mVideoEncoder.encode(out); + } catch (Exception e) { + + } + } + } - byte[] out = YuvUtils.createRGB(image); - getCameraListener().onPictureTaken(out); image.close(); } }; diff --git a/camerakit/src/main/java/com/flurgle/camerakit/CameraView.java b/camerakit/src/main/java/com/flurgle/camerakit/CameraView.java index 7acd6264..f970fe82 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/CameraView.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/CameraView.java @@ -6,8 +6,6 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.pm.PackageManager; import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; @@ -23,11 +21,22 @@ import android.util.AttributeSet; import android.view.Display; import android.widget.FrameLayout; +import com.flurgle.camerakit.utils.DisplayOrientationDetector; + import java.io.File; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import static com.flurgle.camerakit.CameraKit.Constants.*; +import static com.flurgle.camerakit.CameraKit.Constants.FACING_BACK; +import static com.flurgle.camerakit.CameraKit.Constants.FACING_FRONT; +import static com.flurgle.camerakit.CameraKit.Constants.FLASH_AUTO; +import static com.flurgle.camerakit.CameraKit.Constants.FLASH_OFF; +import static com.flurgle.camerakit.CameraKit.Constants.FLASH_ON; +import static com.flurgle.camerakit.CameraKit.Constants.PICTURE_MODE_QUALITY; +import static com.flurgle.camerakit.CameraKit.Constants.PICTURE_MODE_SPEED; +import static com.flurgle.camerakit.CameraKit.Constants.TAP_TO_FOCUS_INVISIBLE; +import static com.flurgle.camerakit.CameraKit.Constants.TAP_TO_FOCUS_OFF; +import static com.flurgle.camerakit.CameraKit.Constants.TAP_TO_FOCUS_VISIBLE; public class CameraView extends FrameLayout { @@ -64,6 +73,7 @@ public class CameraView extends FrameLayout { private boolean mCropOutput; private int mTapToFocus; + private boolean mAutoFocus; private boolean mAdjustViewBounds; @@ -108,6 +118,10 @@ public class CameraView extends FrameLayout { mTapToFocus = a.getInteger(R.styleable.CameraView_ckTapToFocus, TAP_TO_FOCUS_VISIBLE); } + if (attr == R.styleable.CameraView_ckAutoFocus) { + mAutoFocus = a.getBoolean(R.styleable.CameraView_ckAutoFocus, true); + } + if (attr == R.styleable.CameraView_android_adjustViewBounds) { mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false); } @@ -116,7 +130,7 @@ public class CameraView extends FrameLayout { } final PreviewImpl preview = new TextureViewPreview(context, this); - mCameraImpl = new Camera2(context, mCameraListener, preview); + mCameraImpl = new Camera1(mCameraListener, preview); setFacing(mFacing); setFlash(mFlash); @@ -236,6 +250,10 @@ public class CameraView extends FrameLayout { this.mTapToFocus = tapToFocus; } + public void setAutoFocus(boolean autoFocus) { + this.mAutoFocus = autoFocus; + } + public void setCameraListener(CameraListener cameraListener) { this.mCameraListener = new CameraListenerMiddleWare(cameraListener); mCameraImpl.setCameraListener(mCameraListener); @@ -253,11 +271,17 @@ public class CameraView extends FrameLayout { } public void startRecordingVideo() { - + mCameraImpl.startVideo(); } public void stopRecordingVideo() { - + mCameraImpl.endVideo(); + postDelayed(new Runnable() { + @Override + public void run() { + mCameraListener.onVideoTaken(new File(getContext().getExternalFilesDir(null), "video.mp4")); + } + }, 1000); } private void requestCameraPermission() { @@ -351,24 +375,13 @@ public class CameraView extends FrameLayout { @Override public void onPictureTaken(byte[] picture) { super.onPictureTaken(picture); - if (mCropOutput) { - Bitmap bitmap = BitmapFactory.decodeByteArray(picture, 0, picture.length); - int previewWidth = mCameraImpl.mPreview.getWidth(); - int previewHeight = mCameraImpl.mPreview.getWidth(); - - AspectRatio viewRatio = AspectRatio.of(getWidth(), getHeight()); - AspectRatio previewRatio = AspectRatio.of(previewWidth, previewHeight); - - mCameraListener.onPictureTaken(picture); - } else { - mCameraListener.onPictureTaken(picture); - } + mCameraListener.onPictureTaken(picture); } @Override public void onVideoTaken(File video) { super.onVideoTaken(video); - mCameraListener.onCameraOpened(); + mCameraListener.onVideoTaken(video); } } diff --git a/camerakit/src/main/java/com/flurgle/camerakit/CameraViewImpl.java b/camerakit/src/main/java/com/flurgle/camerakit/CameraViewImpl.java index 4855415e..c9c986be 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/CameraViewImpl.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/CameraViewImpl.java @@ -2,7 +2,7 @@ package com.flurgle.camerakit; import android.view.View; -abstract class CameraViewImpl { +public abstract class CameraViewImpl { protected CameraListener mCameraListener; @@ -31,6 +31,8 @@ abstract class CameraViewImpl { abstract int getFlash(); + abstract boolean getAutoFocus(); + abstract void capturePicture(); abstract void captureStill(); diff --git a/camerakit/src/main/java/com/flurgle/camerakit/PictureCaptureCallback.java b/camerakit/src/main/java/com/flurgle/camerakit/PictureCaptureCallback.java index 2880a3c1..bc6297b5 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/PictureCaptureCallback.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/PictureCaptureCallback.java @@ -6,7 +6,7 @@ import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; import android.support.annotation.NonNull; -abstract class PictureCaptureCallback extends CameraCaptureSession.CaptureCallback { +public abstract class PictureCaptureCallback extends CameraCaptureSession.CaptureCallback { static final int STATE_PREVIEW = 0; static final int STATE_LOCKING = 1; diff --git a/camerakit/src/main/java/com/flurgle/camerakit/PreviewImpl.java b/camerakit/src/main/java/com/flurgle/camerakit/PreviewImpl.java index 8fa2947a..3341c78e 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/PreviewImpl.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/PreviewImpl.java @@ -5,7 +5,9 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.View; -abstract class PreviewImpl { +import com.flurgle.camerakit.utils.AspectRatio; + +public abstract class PreviewImpl { interface Callback { void onSurfaceChanged(); @@ -14,9 +16,11 @@ abstract class PreviewImpl { private Callback mCallback; private int mWidth; - private int mHeight; + private int mTrueWidth; + private int mTrueHeight; + void setCallback(Callback callback) { mCallback = callback; } @@ -43,10 +47,19 @@ abstract class PreviewImpl { return null; } - void setBufferSize(int width, int height) { + void setTruePreviewSize(int width, int height) { + this.mTrueWidth = width; + this.mTrueHeight = height; + AspectRatio aspectRatio = AspectRatio.of(width, height); int targetHeight = (int) (getView().getWidth() * aspectRatio.toFloat()); - float scaleY = (float) targetHeight / (float) getView().getHeight(); + float scaleY; + if (getView().getHeight() > 0) { + scaleY = (float) targetHeight / (float) getView().getHeight(); + } else { + scaleY = 1; + } + if (scaleY > 1) { getView().setScaleY(scaleY); } else { @@ -57,6 +70,9 @@ abstract class PreviewImpl { void setSize(int width, int height) { mWidth = width; mHeight = height; + + // Refresh true preview size to adjust scaling + setTruePreviewSize(mTrueWidth, mTrueHeight); } int getWidth() { diff --git a/camerakit/src/main/java/com/flurgle/camerakit/TextureViewPreview.java b/camerakit/src/main/java/com/flurgle/camerakit/TextureViewPreview.java index a2f62800..bca38e64 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/TextureViewPreview.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/TextureViewPreview.java @@ -90,9 +90,11 @@ public class TextureViewPreview extends PreviewImpl { @TargetApi(15) @Override - void setBufferSize(int width, int height) { - super.setBufferSize(width, height); - mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height); + void setTruePreviewSize(int width, int height) { + super.setTruePreviewSize(width, height); + if (mTextureView.getSurfaceTexture() != null) { + mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height); + } } void configureTransform() { diff --git a/camerakit/src/main/java/com/flurgle/camerakit/encoding/Encoder.java b/camerakit/src/main/java/com/flurgle/camerakit/encoding/Encoder.java new file mode 100644 index 00000000..8759c97a --- /dev/null +++ b/camerakit/src/main/java/com/flurgle/camerakit/encoding/Encoder.java @@ -0,0 +1,82 @@ +package com.flurgle.camerakit.encoding; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +public abstract class Encoder { + + protected BlockingQueue queue = new ArrayBlockingQueue(100); + int width; + int height; + int cameraFacing; + long startMS = 0; + byte[] rotatedFrameData = null; + byte[] planeManagedData = null; + boolean encoderStarted = false; + + public Encoder(int cameraFacing, int width, int height) { + this.cameraFacing = cameraFacing; + this.height = height; + this.width = width; + } + + abstract public void encode(byte[] rawData); + + public abstract void stopEncoder(); + + public boolean hasEncoderStarted() { + return encoderStarted; + } + + public void setStartMS(long ms) { + this.startMS = ms; + } + + public static void YV12toYUV420PackedSemiPlanar(final byte[] input, byte[] out, final int width, final int height) { + + final int frameSize = width * height; + final int qFrameSize = frameSize / 4; + + for (int i = 0; i < input.length; i++) { + if (i < frameSize) + out[i] = input[i]; + if (i < (qFrameSize)) { + out[frameSize + i * 2] = input[frameSize + i + qFrameSize]; // Cb (U) + out[frameSize + i * 2 + 1] = input[frameSize + i]; // Cr (V) + } + } + } + + static byte[] NV21toYUV420p(byte[] data, int width, int height) { + int len_target = (width * height * 3) / 2; + byte[] buf_target = new byte[len_target]; + System.arraycopy(data, 0, buf_target, 0, width * height); + + for (int i = 0; i < (width * height / 4); i++) { + buf_target[(width * height) + i] = data[(width * height) + 2 * i + 1]; + buf_target[(width * height) + (width * height / 4) + i] = data[(width * height) + 2 * i]; + } + return buf_target; + } + + public void rotateYUV420Degree90(byte[] data, byte[] output, int imageWidth, int imageHeight) { + int i = 0; + for (int x = 0; x < imageWidth; x++) { + for (int y = imageHeight - 1; y >= 0; y--) { + output[i] = data[y * imageWidth + x]; + i++; + } + } + + i = imageWidth * imageHeight * 3 / 2 - 1; + for (int x = imageWidth - 1; x > 0; x = x - 2) { + for (int y = 0; y < imageHeight / 2; y++) { + output[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x]; + i--; + output[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + (x - 1)]; + i--; + } + } + } + +} \ No newline at end of file diff --git a/camerakit/src/main/java/com/flurgle/camerakit/encoding/Frame.java b/camerakit/src/main/java/com/flurgle/camerakit/encoding/Frame.java new file mode 100644 index 00000000..1fbc7a69 --- /dev/null +++ b/camerakit/src/main/java/com/flurgle/camerakit/encoding/Frame.java @@ -0,0 +1,12 @@ +package com.flurgle.camerakit.encoding; + +public class Frame { + + public int id; + public byte[] frameData; + + public Frame(int id) { + this.id = id; + } + +} \ No newline at end of file diff --git a/camerakit/src/main/java/com/flurgle/camerakit/encoding/VideoEncoder.java b/camerakit/src/main/java/com/flurgle/camerakit/encoding/VideoEncoder.java new file mode 100644 index 00000000..c3163cbd --- /dev/null +++ b/camerakit/src/main/java/com/flurgle/camerakit/encoding/VideoEncoder.java @@ -0,0 +1,157 @@ +package com.flurgle.camerakit.encoding; + +import android.content.Context; +import android.graphics.ImageFormat; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ArrayBlockingQueue; + +public class VideoEncoder extends Encoder { + + File mVideoFile; + FileOutputStream mFOS; + + MediaCodec mMediaCodec; + ByteBuffer[] inputBuffers; + ByteBuffer[] outputBuffers; + MediaFormat mediaFormat = null; + public static int frameID = 0; + + public VideoEncoder(Context context, int cameraFacing, int width, int height) throws IOException { + super(cameraFacing, width, height); + + try { + mVideoFile = new File(context.getExternalFilesDir(null), "video.mp4"); + mFOS = new FileOutputStream(mVideoFile, false); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + queue = new ArrayBlockingQueue<>(100); + + mMediaCodec = MediaCodec.createEncoderByType("video/avc"); + mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000); + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15); + mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5); + mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 8000); + mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + + try { + mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar); + + mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + frameID = 0; + + rotatedFrameData = new byte[width * height * (ImageFormat.getBitsPerPixel(ImageFormat.YV12)) / 8]; + planeManagedData = new byte[width * height * (ImageFormat.getBitsPerPixel(ImageFormat.YV12)) / 8]; + + encoderStarted = true; + mMediaCodec.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + public void stopEncoder() { + encoderStarted = false; + if (mMediaCodec != null) { + mMediaCodec.stop(); + mMediaCodec.release(); + mMediaCodec = null; + } + + if (mFOS != null) { + try { + mFOS.close(); + } catch (IOException e) { + + } + } + } + + + @Override + public void encode(byte[] rawData) { + inputBuffers = mMediaCodec.getInputBuffers(); + outputBuffers = mMediaCodec.getOutputBuffers(); + + int inputBufferIndex = mMediaCodec.dequeueInputBuffer(0); + if (inputBufferIndex >= 0) { + ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; + inputBuffer.clear(); + + int size = inputBuffer.limit(); + inputBuffer.put(rawData); + + mMediaCodec.queueInputBuffer(inputBufferIndex, 0, size, (System.currentTimeMillis() - startMS) * 1000, 0); + } else { + return; + } + + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0); + do { + if (outputBufferIndex >= 0) { + Frame frame = new Frame(frameID); + ByteBuffer outBuffer = outputBuffers[outputBufferIndex]; + byte[] outData = new byte[bufferInfo.size]; + int dataLength = 0; + + outBuffer.get(outData); + + dataLength = outData.length - 2; + frame.frameData = new byte[dataLength]; + // skipping 0x00 0x80 while copying + System.arraycopy(outData, 2, frame.frameData, 0, dataLength); + + try { + if (bufferInfo.offset != 0) { + mFOS.write(outData, bufferInfo.offset, outData.length - bufferInfo.offset); + } else { + mFOS.write(outData, 0, outData.length); + } + mFOS.flush(); + } catch (IOException e) { + Log.e("Encoding", e.toString()); + } + + try { + queue.put(frame); + } catch (InterruptedException e) { + Log.e("EncodeDecode", "interrupted while waiting"); + e.printStackTrace(); + } catch (NullPointerException e) { + Log.e("EncodeDecode", "frame is null"); + e.printStackTrace(); + } catch (IllegalArgumentException e) { + Log.e("EncodeDecode", "problem inserting in the queue"); + e.printStackTrace(); + } + Log.d("EncodeDecode", "H263 frame enqueued. queue size now: " + queue.size()); + + frameID++; + mMediaCodec.releaseOutputBuffer(outputBufferIndex, false); + outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0); + + } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + outputBuffers = mMediaCodec.getOutputBuffers(); + Log.e("EncodeDecode", "output buffer of encoder : info changed"); + } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + Log.e("EncodeDecode", "output buffer of encoder : format changed"); + } else { + Log.e("EncodeDecode", "unknown value of outputBufferIndex : " + outputBufferIndex); + } + } while (outputBufferIndex >= 0); + } + + +} diff --git a/camerakit/src/main/java/com/flurgle/camerakit/AspectRatio.java b/camerakit/src/main/java/com/flurgle/camerakit/utils/AspectRatio.java similarity index 99% rename from camerakit/src/main/java/com/flurgle/camerakit/AspectRatio.java rename to camerakit/src/main/java/com/flurgle/camerakit/utils/AspectRatio.java index 42c72f6d..c12b2b90 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/AspectRatio.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/utils/AspectRatio.java @@ -1,4 +1,4 @@ -package com.flurgle.camerakit; +package com.flurgle.camerakit.utils; import android.os.Parcel; import android.os.Parcelable; diff --git a/camerakit/src/main/java/com/flurgle/camerakit/DisplayOrientationDetector.java b/camerakit/src/main/java/com/flurgle/camerakit/utils/DisplayOrientationDetector.java similarity index 96% rename from camerakit/src/main/java/com/flurgle/camerakit/DisplayOrientationDetector.java rename to camerakit/src/main/java/com/flurgle/camerakit/utils/DisplayOrientationDetector.java index 128c74f1..29d98a4b 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/DisplayOrientationDetector.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/utils/DisplayOrientationDetector.java @@ -1,4 +1,4 @@ -package com.flurgle.camerakit; +package com.flurgle.camerakit.utils; import android.content.Context; import android.util.SparseIntArray; @@ -6,7 +6,7 @@ import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; -abstract class DisplayOrientationDetector { +public abstract class DisplayOrientationDetector { private final OrientationEventListener mOrientationEventListener; diff --git a/camerakit/src/main/java/com/flurgle/camerakit/Size.java b/camerakit/src/main/java/com/flurgle/camerakit/utils/Size.java similarity index 96% rename from camerakit/src/main/java/com/flurgle/camerakit/Size.java rename to camerakit/src/main/java/com/flurgle/camerakit/utils/Size.java index 1a03668d..dd011363 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/Size.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/utils/Size.java @@ -1,4 +1,4 @@ -package com.flurgle.camerakit; +package com.flurgle.camerakit.utils; import android.support.annotation.NonNull; diff --git a/camerakit/src/main/java/com/flurgle/camerakit/YuvUtils.java b/camerakit/src/main/java/com/flurgle/camerakit/utils/YuvUtils.java similarity index 71% rename from camerakit/src/main/java/com/flurgle/camerakit/YuvUtils.java rename to camerakit/src/main/java/com/flurgle/camerakit/utils/YuvUtils.java index 0aa54f6f..d09ea710 100644 --- a/camerakit/src/main/java/com/flurgle/camerakit/YuvUtils.java +++ b/camerakit/src/main/java/com/flurgle/camerakit/utils/YuvUtils.java @@ -1,9 +1,10 @@ -package com.flurgle.camerakit; +package com.flurgle.camerakit.utils; import android.graphics.ImageFormat; import android.graphics.Rect; import android.graphics.YuvImage; import android.media.Image; +import android.support.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -11,7 +12,22 @@ import java.nio.ByteBuffer; public class YuvUtils { - public static byte[] createRGB(Image image) { + public static byte[] createRGB(Image image, @Nullable Rect crop) { + byte[] data = getYUVData(image); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, image.getWidth(), image.getHeight(), null); + + if (crop == null) { + crop = new Rect(0, 0, image.getWidth(), image.getHeight()); + } + + yuvImage.compressToJpeg(crop, 50, out); + + return out.toByteArray(); + } + + public static byte[] getYUVData(Image image) { ByteBuffer bufferY = image.getPlanes()[0].getBuffer(); byte[] y = new byte[bufferY.remaining()]; bufferY.get(y); @@ -32,13 +48,7 @@ public class YuvUtils { } catch (IOException e) { } - byte data[] = outputStream.toByteArray(); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, image.getWidth(), image.getHeight(), null); - yuvImage.compressToJpeg(new Rect(0, 0, image.getWidth(), image.getHeight()), 50, out); - - return out.toByteArray(); + return outputStream.toByteArray(); } } diff --git a/camerakit/src/main/res/values/attrs.xml b/camerakit/src/main/res/values/attrs.xml index 5b474faa..154736ac 100644 --- a/camerakit/src/main/res/values/attrs.xml +++ b/camerakit/src/main/res/values/attrs.xml @@ -28,6 +28,8 @@ + + diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index ffa019b8..513e9ebf 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -9,13 +9,16 @@ android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/AppTheme"> - + + - + \ No newline at end of file diff --git a/demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java b/demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java index a6496705..6d620278 100644 --- a/demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java +++ b/demo/src/main/java/com/flurgle/camerakit/demo/MainActivity.java @@ -18,6 +18,8 @@ import com.flurgle.camerakit.CameraKit; import com.flurgle.camerakit.CameraListener; import com.flurgle.camerakit.CameraView; +import java.io.File; + import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnCheckedChanged; @@ -110,7 +112,22 @@ public class MainActivity extends AppCompatActivity { @OnClick(R.id.captureVideo) void captureVideo() { + camera.startRecordingVideo(); + camera.postDelayed(new Runnable() { + @Override + public void run() { + camera.stopRecordingVideo(); + } + }, 3000); + camera.setCameraListener(new CameraListener() { + @Override + public void onVideoTaken(File video) { + super.onVideoTaken(video); + PreviewDialog previewDialog = new PreviewDialog(MainActivity.this, video); + previewDialog.show(); + } + }); } @OnClick(R.id.toggleCamera) @@ -163,7 +180,7 @@ public class MainActivity extends AppCompatActivity { invalidateParameters(); } - @OnFocusChange({ R.id.width, R.id.height }) + @OnFocusChange({R.id.width, R.id.height}) void inputFocusChanged(View view, boolean f) { blockInvalidation = view.isFocused(); } diff --git a/demo/src/main/java/com/flurgle/camerakit/demo/PreviewDialog.java b/demo/src/main/java/com/flurgle/camerakit/demo/PreviewDialog.java index a017e7ad..68713925 100644 --- a/demo/src/main/java/com/flurgle/camerakit/demo/PreviewDialog.java +++ b/demo/src/main/java/com/flurgle/camerakit/demo/PreviewDialog.java @@ -3,9 +3,14 @@ package com.flurgle.camerakit.demo; import android.app.Dialog; import android.content.Context; import android.graphics.Bitmap; +import android.media.MediaPlayer; import android.os.Bundle; import android.support.annotation.NonNull; +import android.view.View; import android.widget.ImageView; +import android.widget.VideoView; + +import java.io.File; import butterknife.BindView; import butterknife.ButterKnife; @@ -13,22 +18,44 @@ import butterknife.ButterKnife; public class PreviewDialog extends Dialog { @BindView(R.id.image) - ImageView image; + ImageView imageView; + + @BindView(R.id.video) + VideoView videoView; private Bitmap bitmap; + private File video; public PreviewDialog(@NonNull Context context, Bitmap bitmap) { super(context); this.bitmap = bitmap; } + public PreviewDialog(@NonNull Context context, File video) { + super(context); + this.video = video; + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.dialog_preview); ButterKnife.bind(this); - image.setImageBitmap(bitmap); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else if (video != null) { + imageView.setVisibility(View.GONE); + videoView.setVideoPath(video.getAbsolutePath()); + videoView.setMediaController(null); + videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { + @Override + public void onPrepared(MediaPlayer mediaPlayer) { + mediaPlayer.setLooping(true); + } + }); + videoView.start(); + } } } diff --git a/demo/src/main/res/layout/activity_main.xml b/demo/src/main/res/layout/activity_main.xml index fde00098..73b889be 100644 --- a/demo/src/main/res/layout/activity_main.xml +++ b/demo/src/main/res/layout/activity_main.xml @@ -16,12 +16,12 @@ + app:ckPictureMode="quality" />