| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.media; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.graphics.ImageFormat; |
| import android.graphics.Rect; |
| import android.graphics.SurfaceTexture; |
| import android.opengl.GLES20; |
| import android.os.Build; |
| import android.util.SparseArray; |
| |
| import org.chromium.base.Log; |
| import org.chromium.base.annotations.JNINamespace; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| /** |
| * Video Capture Device extension of VideoCapture to provide common functionality |
| * for capture using android.hardware.Camera API (deprecated in API 21). For Normal |
| * Android devices, it provides functionality for receiving copies of preview |
| * frames via Java-allocated buffers. It also includes class BuggyDeviceHack to |
| * deal with troublesome devices. |
| **/ |
| @JNINamespace("media") |
| @SuppressWarnings("deprecation") |
| // TODO: is this class only used on ICS MR1 (or some later version) and above? |
| @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) |
| public class VideoCaptureCamera |
| extends VideoCapture implements android.hardware.Camera.PreviewCallback { |
| private static final String TAG = "VideoCapture"; |
| private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65; |
| private static final int NUM_CAPTURE_BUFFERS = 3; |
| |
| // Map of the equivalent color temperature in Kelvin for the White Balance setting. The |
| // values are a mixture of educated guesses and data from Android's Camera2 API. The |
| // temperatures must be ordered increasingly. |
| private static final SparseArray<String> COLOR_TEMPERATURES_MAP; |
| static { |
| COLOR_TEMPERATURES_MAP = new SparseArray<String>(); |
| COLOR_TEMPERATURES_MAP.append( |
| 2850, android.hardware.Camera.Parameters.WHITE_BALANCE_INCANDESCENT); |
| COLOR_TEMPERATURES_MAP.append( |
| 2940, android.hardware.Camera.Parameters.WHITE_BALANCE_WARM_FLUORESCENT); |
| COLOR_TEMPERATURES_MAP.append( |
| 3000, android.hardware.Camera.Parameters.WHITE_BALANCE_TWILIGHT); |
| COLOR_TEMPERATURES_MAP.append( |
| 4230, android.hardware.Camera.Parameters.WHITE_BALANCE_FLUORESCENT); |
| COLOR_TEMPERATURES_MAP.append( |
| 6000, android.hardware.Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT); |
| COLOR_TEMPERATURES_MAP.append( |
| 6504, android.hardware.Camera.Parameters.WHITE_BALANCE_DAYLIGHT); |
| COLOR_TEMPERATURES_MAP.append(7000, android.hardware.Camera.Parameters.WHITE_BALANCE_SHADE); |
| }; |
| |
| // Some devices don't support YV12 format correctly, even with JELLY_BEAN or |
| // newer OS. To work around the issues on those devices, we have to request |
| // NV21. This is supposed to be a temporary hack. |
| private static class BuggyDeviceHack { |
| private static final String[] COLORSPACE_BUGGY_DEVICE_LIST = { |
| "SAMSUNG-SGH-I747", "ODROID-U2", |
| // See https://crbug.com/577435 for more info. |
| "XT1092", "XT1095", "XT1096", "XT1097", |
| }; |
| |
| static int getImageFormat() { |
| for (String buggyDevice : COLORSPACE_BUGGY_DEVICE_LIST) { |
| if (buggyDevice.contentEquals(android.os.Build.MODEL)) { |
| return ImageFormat.NV21; |
| } |
| } |
| return ImageFormat.YV12; |
| } |
| } |
| |
| private int mExpectedFrameSize; |
| private final Object mPhotoTakenCallbackLock = new Object(); |
| |
| // Storage of takePicture() callback Id. There can be one such request in flight at most, and |
| // needs to be exercised either in case of error or sucess. |
| private long mPhotoTakenCallbackId; |
| private int mPhotoWidth; |
| private int mPhotoHeight; |
| private android.hardware.Camera.Area mAreaOfInterest; |
| |
| private android.hardware.Camera mCamera; |
| // Lock to mutually exclude execution of OnPreviewFrame() and {start/stop}Capture(). |
| private ReentrantLock mPreviewBufferLock = new ReentrantLock(); |
| // True when native code has started capture. |
| private boolean mIsRunning; |
| |
| private int[] mGlTextures; |
| private SurfaceTexture mSurfaceTexture; |
| |
| private static android.hardware.Camera.CameraInfo getCameraInfo(int id) { |
| android.hardware.Camera.CameraInfo cameraInfo = new android.hardware.Camera.CameraInfo(); |
| try { |
| android.hardware.Camera.getCameraInfo(id, cameraInfo); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "getCameraInfo: Camera.getCameraInfo: " + ex); |
| return null; |
| } |
| return cameraInfo; |
| } |
| |
| private static android.hardware.Camera.Parameters getCameraParameters( |
| android.hardware.Camera camera) { |
| android.hardware.Camera.Parameters parameters; |
| try { |
| parameters = camera.getParameters(); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "getCameraParameters: android.hardware.Camera.getParameters: " + ex); |
| if (camera != null) camera.release(); |
| return null; |
| } |
| return parameters; |
| } |
| |
| private String getClosestWhiteBalance(int colorTemperature) { |
| int minDiff = Integer.MAX_VALUE; |
| String matchedTemperature = null; |
| |
| for (int i = 0; i < COLOR_TEMPERATURES_MAP.size(); ++i) { |
| final int diff = Math.abs(colorTemperature - COLOR_TEMPERATURES_MAP.keyAt(i)); |
| if (diff >= minDiff) continue; |
| minDiff = diff; |
| matchedTemperature = COLOR_TEMPERATURES_MAP.valueAt(i); |
| } |
| return matchedTemperature; |
| } |
| |
| private class CrErrorCallback implements android.hardware.Camera.ErrorCallback { |
| @Override |
| public void onError(int error, android.hardware.Camera camera) { |
| nativeOnError(mNativeVideoCaptureDeviceAndroid, "Error id: " + error); |
| |
| synchronized (mPhotoTakenCallbackLock) { |
| if (mPhotoTakenCallbackId == 0) return; |
| nativeOnPhotoTaken( |
| mNativeVideoCaptureDeviceAndroid, mPhotoTakenCallbackId, new byte[0]); |
| mPhotoTakenCallbackId = 0; |
| } |
| } |
| } |
| |
| private class CrPictureCallback implements android.hardware.Camera.PictureCallback { |
| @Override |
| public void onPictureTaken(byte[] data, android.hardware.Camera camera) { |
| synchronized (mPhotoTakenCallbackLock) { |
| if (mPhotoTakenCallbackId != 0) { |
| nativeOnPhotoTaken( |
| mNativeVideoCaptureDeviceAndroid, mPhotoTakenCallbackId, data); |
| } |
| mPhotoTakenCallbackId = 0; |
| } |
| android.hardware.Camera.Parameters parameters = getCameraParameters(mCamera); |
| parameters.setRotation(0); |
| mCamera.setParameters(parameters); |
| camera.startPreview(); |
| } |
| }; |
| |
| static int getNumberOfCameras() { |
| return android.hardware.Camera.getNumberOfCameras(); |
| } |
| |
| static int getCaptureApiType(int id) { |
| if (VideoCaptureCamera.getCameraInfo(id) == null) { |
| return VideoCaptureApi.UNKNOWN; |
| } |
| return VideoCaptureApi.ANDROID_API1; |
| } |
| |
| static String getName(int id) { |
| android.hardware.Camera.CameraInfo cameraInfo = VideoCaptureCamera.getCameraInfo(id); |
| if (cameraInfo == null) return null; |
| |
| return "camera " + id + ", facing " |
| + (cameraInfo.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT |
| ? "front" |
| : "back"); |
| } |
| |
| static VideoCaptureFormat[] getDeviceSupportedFormats(int id) { |
| android.hardware.Camera camera; |
| try { |
| camera = android.hardware.Camera.open(id); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "Camera.open: ", ex); |
| return null; |
| } |
| android.hardware.Camera.Parameters parameters = getCameraParameters(camera); |
| if (parameters == null) { |
| return null; |
| } |
| |
| ArrayList<VideoCaptureFormat> formatList = new ArrayList<VideoCaptureFormat>(); |
| // getSupportedPreview{Formats,FpsRange,PreviewSizes}() returns Lists |
| // with at least one element, but when the camera is in bad state, they |
| // can return null pointers; in that case we use a 0 entry, so we can |
| // retrieve as much information as possible. |
| List<Integer> pixelFormats = parameters.getSupportedPreviewFormats(); |
| if (pixelFormats == null) { |
| pixelFormats = new ArrayList<Integer>(); |
| } |
| if (pixelFormats.size() == 0) { |
| pixelFormats.add(ImageFormat.UNKNOWN); |
| } |
| for (Integer previewFormat : pixelFormats) { |
| int pixelFormat = AndroidImageFormat.UNKNOWN; |
| if (previewFormat == ImageFormat.YV12) { |
| pixelFormat = AndroidImageFormat.YV12; |
| } else if (previewFormat == ImageFormat.NV21) { |
| continue; |
| } |
| |
| List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange(); |
| if (listFpsRange == null) { |
| listFpsRange = new ArrayList<int[]>(); |
| } |
| if (listFpsRange.size() == 0) { |
| listFpsRange.add(new int[] {0, 0}); |
| } |
| for (int[] fpsRange : listFpsRange) { |
| List<android.hardware.Camera.Size> supportedSizes = |
| parameters.getSupportedPreviewSizes(); |
| if (supportedSizes == null) { |
| supportedSizes = new ArrayList<android.hardware.Camera.Size>(); |
| } |
| if (supportedSizes.size() == 0) { |
| supportedSizes.add(camera.new Size(0, 0)); |
| } |
| for (android.hardware.Camera.Size size : supportedSizes) { |
| formatList.add(new VideoCaptureFormat( |
| size.width, size.height, (fpsRange[1] + 999) / 1000, pixelFormat)); |
| } |
| } |
| } |
| camera.release(); |
| return formatList.toArray(new VideoCaptureFormat[formatList.size()]); |
| } |
| |
| VideoCaptureCamera(Context context, int id, long nativeVideoCaptureDeviceAndroid) { |
| super(context, id, nativeVideoCaptureDeviceAndroid); |
| } |
| |
| @Override |
| public boolean allocate(int width, int height, int frameRate) { |
| Log.d(TAG, "allocate: requested (%d x %d) @%dfps", width, height, frameRate); |
| try { |
| mCamera = android.hardware.Camera.open(mId); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "allocate: Camera.open: " + ex); |
| return false; |
| } |
| |
| android.hardware.Camera.CameraInfo cameraInfo = VideoCaptureCamera.getCameraInfo(mId); |
| if (cameraInfo == null) { |
| mCamera.release(); |
| mCamera = null; |
| return false; |
| } |
| mCameraNativeOrientation = cameraInfo.orientation; |
| // For Camera API, the readings of back-facing camera need to be inverted. |
| mInvertDeviceOrientationReadings = |
| (cameraInfo.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK); |
| Log.d(TAG, "allocate: Rotation dev=%d, cam=%d, facing back? %s", getDeviceRotation(), |
| mCameraNativeOrientation, mInvertDeviceOrientationReadings); |
| |
| android.hardware.Camera.Parameters parameters = getCameraParameters(mCamera); |
| if (parameters == null) { |
| mCamera = null; |
| return false; |
| } |
| |
| // getSupportedPreviewFpsRange() returns a List with at least one |
| // element, but when camera is in bad state, it can return null pointer. |
| List<int[]> listFpsRange = parameters.getSupportedPreviewFpsRange(); |
| if (listFpsRange == null || listFpsRange.size() == 0) { |
| Log.e(TAG, "allocate: no fps range found"); |
| return false; |
| } |
| final ArrayList<FramerateRange> framerateRanges = |
| new ArrayList<FramerateRange>(listFpsRange.size()); |
| for (int[] range : listFpsRange) { |
| framerateRanges.add(new FramerateRange(range[0], range[1])); |
| } |
| // API fps ranges are scaled up x1000 to avoid floating point. |
| int frameRateScaled = frameRate * 1000; |
| final FramerateRange chosenFramerateRange = |
| getClosestFramerateRange(framerateRanges, frameRateScaled); |
| final int[] chosenFpsRange = new int[] {chosenFramerateRange.min, chosenFramerateRange.max}; |
| Log.d(TAG, "allocate: fps set to [%d-%d]", chosenFpsRange[0], chosenFpsRange[1]); |
| |
| // Calculate size. |
| List<android.hardware.Camera.Size> listCameraSize = parameters.getSupportedPreviewSizes(); |
| int minDiff = Integer.MAX_VALUE; |
| int matchedWidth = width; |
| int matchedHeight = height; |
| for (android.hardware.Camera.Size size : listCameraSize) { |
| int diff = Math.abs(size.width - width) + Math.abs(size.height - height); |
| Log.d(TAG, "allocate: supported (%d, %d), diff=%d", size.width, size.height, diff); |
| // TODO(wjia): Remove this hack (forcing width to be multiple |
| // of 32) by supporting stride in video frame buffer. |
| // Right now, VideoCaptureController requires compact YV12 |
| // (i.e., with no padding). |
| if (diff < minDiff && (size.width % 32 == 0)) { |
| minDiff = diff; |
| matchedWidth = size.width; |
| matchedHeight = size.height; |
| } |
| } |
| if (minDiff == Integer.MAX_VALUE) { |
| Log.e(TAG, "allocate: can not find a multiple-of-32 resolution"); |
| return false; |
| } |
| Log.d(TAG, "allocate: matched (%d x %d)", matchedWidth, matchedHeight); |
| |
| if (parameters.isVideoStabilizationSupported()) { |
| Log.d(TAG, "Image stabilization supported, currently: " |
| + parameters.getVideoStabilization() + ", setting it."); |
| parameters.setVideoStabilization(true); |
| } else { |
| Log.d(TAG, "Image stabilization not supported."); |
| } |
| |
| if (parameters.getSupportedFocusModes().contains( |
| android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { |
| parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); |
| } else { |
| Log.d(TAG, "Continuous focus mode not supported."); |
| } |
| |
| mCaptureFormat = new VideoCaptureFormat(matchedWidth, matchedHeight, |
| chosenFpsRange[1] / 1000, BuggyDeviceHack.getImageFormat()); |
| parameters.setPictureSize(matchedWidth, matchedHeight); |
| parameters.setPreviewSize(matchedWidth, matchedHeight); |
| parameters.setPreviewFpsRange(chosenFpsRange[0], chosenFpsRange[1]); |
| parameters.setPreviewFormat(mCaptureFormat.mPixelFormat); |
| try { |
| mCamera.setParameters(parameters); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "setParameters: " + ex); |
| return false; |
| } |
| |
| // Set SurfaceTexture. Android Capture needs a SurfaceTexture even if |
| // it is not going to be used. |
| mGlTextures = new int[1]; |
| // Generate one texture pointer and bind it as an external texture. |
| GLES20.glGenTextures(1, mGlTextures, 0); |
| GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, mGlTextures[0]); |
| // No mip-mapping with camera source. |
| GLES20.glTexParameterf( |
| GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); |
| GLES20.glTexParameterf( |
| GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); |
| // Clamp to edge is only option. |
| GLES20.glTexParameteri( |
| GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); |
| GLES20.glTexParameteri( |
| GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); |
| |
| mSurfaceTexture = new SurfaceTexture(mGlTextures[0]); |
| mSurfaceTexture.setOnFrameAvailableListener(null); |
| try { |
| mCamera.setPreviewTexture(mSurfaceTexture); |
| } catch (IOException ex) { |
| Log.e(TAG, "allocate: " + ex); |
| return false; |
| } |
| |
| mCamera.setErrorCallback(new CrErrorCallback()); |
| |
| mExpectedFrameSize = mCaptureFormat.mWidth * mCaptureFormat.mHeight |
| * ImageFormat.getBitsPerPixel(mCaptureFormat.mPixelFormat) / 8; |
| for (int i = 0; i < NUM_CAPTURE_BUFFERS; i++) { |
| byte[] buffer = new byte[mExpectedFrameSize]; |
| mCamera.addCallbackBuffer(buffer); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean startCapture() { |
| if (mCamera == null) { |
| Log.e(TAG, "startCapture: mCamera is null"); |
| return false; |
| } |
| |
| mPreviewBufferLock.lock(); |
| try { |
| if (mIsRunning) { |
| return true; |
| } |
| } finally { |
| mPreviewBufferLock.unlock(); |
| } |
| |
| setPreviewCallback(this); |
| try { |
| mCamera.startPreview(); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "startCapture: Camera.startPreview: " + ex); |
| return false; |
| } |
| |
| mPreviewBufferLock.lock(); |
| try { |
| nativeOnStarted(mNativeVideoCaptureDeviceAndroid); |
| mIsRunning = true; |
| } finally { |
| mPreviewBufferLock.unlock(); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean stopCapture() { |
| if (mCamera == null) { |
| Log.e(TAG, "stopCapture: mCamera is null"); |
| return true; |
| } |
| |
| mPreviewBufferLock.lock(); |
| try { |
| if (!mIsRunning) { |
| return true; |
| } |
| mIsRunning = false; |
| } finally { |
| mPreviewBufferLock.unlock(); |
| } |
| |
| mCamera.stopPreview(); |
| setPreviewCallback(null); |
| return true; |
| } |
| |
| @Override |
| public PhotoCapabilities getPhotoCapabilities() { |
| final android.hardware.Camera.Parameters parameters = getCameraParameters(mCamera); |
| PhotoCapabilities.Builder builder = new PhotoCapabilities.Builder(); |
| Log.i(TAG, " CAM params: %s", parameters.flatten()); |
| |
| // Before the Camera2 API there was no official way to retrieve the supported, if any, ISO |
| // values from |parameters|; some platforms had "iso-values", others "iso-mode-values" etc. |
| // Ignore them. |
| builder.setMinIso(0).setMaxIso(0).setCurrentIso(0).setStepIso(0); |
| |
| List<android.hardware.Camera.Size> supportedSizes = parameters.getSupportedPictureSizes(); |
| int minWidth = Integer.MAX_VALUE; |
| int minHeight = Integer.MAX_VALUE; |
| int maxWidth = 0; |
| int maxHeight = 0; |
| for (android.hardware.Camera.Size size : supportedSizes) { |
| if (size.width < minWidth) minWidth = size.width; |
| if (size.height < minHeight) minHeight = size.height; |
| if (size.width > maxWidth) maxWidth = size.width; |
| if (size.height > maxHeight) maxHeight = size.height; |
| } |
| builder.setMinHeight(minHeight).setMaxHeight(maxHeight).setStepHeight(1); |
| builder.setMinWidth(minWidth).setMaxWidth(maxWidth).setStepWidth(1); |
| final android.hardware.Camera.Size currentSize = parameters.getPreviewSize(); |
| builder.setCurrentHeight(currentSize.height).setCurrentWidth(currentSize.width); |
| |
| int maxZoom = 0; |
| int currentZoom = 0; |
| int minZoom = 0; |
| int stepZoom = 0; |
| if (parameters.isZoomSupported()) { |
| maxZoom = parameters.getZoomRatios().get(parameters.getMaxZoom()); |
| currentZoom = parameters.getZoomRatios().get(parameters.getZoom()); |
| minZoom = parameters.getZoomRatios().get(0); |
| if (parameters.getZoomRatios().size() > 1) { |
| stepZoom = parameters.getZoomRatios().get(1) - parameters.getZoomRatios().get(0); |
| } |
| } |
| builder.setMinZoom(minZoom).setMaxZoom(maxZoom); |
| builder.setCurrentZoom(currentZoom).setStepZoom(stepZoom); |
| |
| Log.d(TAG, "parameters.getFocusMode(): %s", parameters.getFocusMode()); |
| final String focusMode = parameters.getFocusMode(); |
| |
| // Classify the Focus capabilities. In CONTINUOUS and SINGLE_SHOT, we can call |
| // autoFocus(AutoFocusCallback) to configure region(s) to focus onto. |
| int jniFocusMode = AndroidMeteringMode.NONE; |
| if (focusMode.equals(android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO) |
| || focusMode.equals( |
| android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) |
| || focusMode.equals(android.hardware.Camera.Parameters.FOCUS_MODE_EDOF)) { |
| jniFocusMode = AndroidMeteringMode.CONTINUOUS; |
| } else if (focusMode.equals(android.hardware.Camera.Parameters.FOCUS_MODE_AUTO) |
| || focusMode.equals(android.hardware.Camera.Parameters.FOCUS_MODE_MACRO)) { |
| jniFocusMode = AndroidMeteringMode.SINGLE_SHOT; |
| } else if (focusMode.equals(android.hardware.Camera.Parameters.FOCUS_MODE_INFINITY) |
| || focusMode.equals(android.hardware.Camera.Parameters.FOCUS_MODE_FIXED)) { |
| jniFocusMode = AndroidMeteringMode.FIXED; |
| } |
| builder.setFocusMode(jniFocusMode); |
| |
| // Exposure is usually continuously updated except it not available at all, or if the |
| // exposure compensation is locked, in which case we consider it as FIXED. |
| int jniExposureMode = parameters.getMaxNumMeteringAreas() == 0 |
| ? AndroidMeteringMode.NONE |
| : AndroidMeteringMode.CONTINUOUS; |
| if (parameters.isAutoExposureLockSupported() && parameters.getAutoExposureLock()) { |
| jniExposureMode = AndroidMeteringMode.FIXED; |
| } |
| builder.setExposureMode(jniExposureMode); |
| |
| final float step = parameters.getExposureCompensationStep(); |
| builder.setStepExposureCompensation(step); |
| builder.setMinExposureCompensation(parameters.getMinExposureCompensation() * step); |
| builder.setMaxExposureCompensation(parameters.getMaxExposureCompensation() * step); |
| builder.setCurrentExposureCompensation(parameters.getExposureCompensation() * step); |
| |
| int jniWhiteBalanceMode = AndroidMeteringMode.NONE; |
| if (parameters.isAutoWhiteBalanceLockSupported() |
| && parameters.getSupportedWhiteBalance() != null) { |
| jniWhiteBalanceMode = parameters.getWhiteBalance() |
| == android.hardware.Camera.Parameters.WHITE_BALANCE_AUTO |
| ? AndroidMeteringMode.CONTINUOUS |
| : AndroidMeteringMode.FIXED; |
| } |
| builder.setWhiteBalanceMode(jniWhiteBalanceMode); |
| |
| builder.setMinColorTemperature(COLOR_TEMPERATURES_MAP.keyAt(0)); |
| builder.setMaxColorTemperature( |
| COLOR_TEMPERATURES_MAP.keyAt(COLOR_TEMPERATURES_MAP.size() - 1)); |
| if (jniWhiteBalanceMode == AndroidMeteringMode.FIXED) { |
| final int index = COLOR_TEMPERATURES_MAP.indexOfValue(parameters.getWhiteBalance()); |
| if (index >= 0) builder.setCurrentColorTemperature(COLOR_TEMPERATURES_MAP.keyAt(index)); |
| } |
| |
| final List<String> flashModes = parameters.getSupportedFlashModes(); |
| if (flashModes != null) { |
| builder.setSupportsTorch( |
| flashModes.contains(android.hardware.Camera.Parameters.FLASH_MODE_TORCH)); |
| builder.setTorch(parameters.getFlashMode() |
| == android.hardware.Camera.Parameters.FLASH_MODE_TORCH); |
| |
| builder.setRedEyeReduction( |
| flashModes.contains(android.hardware.Camera.Parameters.FLASH_MODE_RED_EYE)); |
| |
| ArrayList<Integer> modes = new ArrayList<Integer>(0); |
| if (flashModes.contains(android.hardware.Camera.Parameters.FLASH_MODE_OFF)) { |
| modes.add(Integer.valueOf(AndroidFillLightMode.OFF)); |
| } |
| if (flashModes.contains(android.hardware.Camera.Parameters.FLASH_MODE_AUTO)) { |
| modes.add(Integer.valueOf(AndroidFillLightMode.AUTO)); |
| } |
| if (flashModes.contains(android.hardware.Camera.Parameters.FLASH_MODE_ON)) { |
| modes.add(Integer.valueOf(AndroidFillLightMode.FLASH)); |
| } |
| |
| int[] modesAsIntArray = new int[modes.size()]; |
| for (int i = 0; i < modes.size(); i++) modesAsIntArray[i] = modes.get(i).intValue(); |
| builder.setFillLightModes(modesAsIntArray); |
| } |
| |
| return builder.build(); |
| } |
| |
| @Override |
| public void setPhotoOptions(double zoom, int focusMode, int exposureMode, double width, |
| double height, float[] pointsOfInterest2D, boolean hasExposureCompensation, |
| double exposureCompensation, int whiteBalanceMode, double iso, |
| boolean hasRedEyeReduction, boolean redEyeReduction, int fillLightMode, |
| boolean hasTorch, boolean torch, double colorTemperature) { |
| android.hardware.Camera.Parameters parameters = getCameraParameters(mCamera); |
| |
| if (parameters.isZoomSupported() && zoom > 0) { |
| // |zoomRatios| is an ordered list; need the closest zoom index for parameters.setZoom() |
| final List<Integer> zoomRatios = parameters.getZoomRatios(); |
| int i = 1; |
| for (; i < zoomRatios.size(); ++i) { |
| if (zoom < zoomRatios.get(i)) { |
| break; |
| } |
| } |
| parameters.setZoom(i - 1); |
| } |
| |
| if (focusMode == AndroidMeteringMode.FIXED) { |
| parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_FIXED); |
| } else if (focusMode == AndroidMeteringMode.SINGLE_SHOT) { |
| parameters.setFocusMode(android.hardware.Camera.Parameters.FOCUS_MODE_AUTO); |
| } else if (focusMode == AndroidMeteringMode.CONTINUOUS) { |
| parameters.setFocusMode( |
| android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); |
| } |
| |
| if (parameters.isAutoExposureLockSupported()) { |
| if (exposureMode == AndroidMeteringMode.FIXED) { |
| parameters.setAutoExposureLock(true); |
| } else if (exposureMode != AndroidMeteringMode.NONE) { |
| parameters.setAutoExposureLock(false); |
| } |
| } |
| if (width > 0) mPhotoWidth = (int) Math.round(width); |
| if (height > 0) mPhotoHeight = (int) Math.round(height); |
| |
| // Upon new |zoom| configuration, clear up the previous |mAreaOfInterest| if any. |
| if (mAreaOfInterest != null && !mAreaOfInterest.rect.isEmpty() && zoom > 0) { |
| mAreaOfInterest = null; |
| } |
| // Also clear |mAreaOfInterest| if the user sets it as NONE. |
| if (focusMode == AndroidMeteringMode.NONE || exposureMode == AndroidMeteringMode.NONE) { |
| mAreaOfInterest = null; |
| } |
| |
| // Update |mAreaOfInterest| if the camera supports and there are |pointsOfInterest2D|. |
| final boolean pointsOfInterestSupported = |
| parameters.getMaxNumMeteringAreas() > 0 || parameters.getMaxNumFocusAreas() > 0; |
| if (pointsOfInterestSupported && pointsOfInterest2D.length > 0) { |
| assert pointsOfInterest2D.length == 1 : "Only 1 point of interest supported"; |
| assert pointsOfInterest2D[0] <= 1.0 && pointsOfInterest2D[0] >= 0.0; |
| assert pointsOfInterest2D[1] <= 1.0 && pointsOfInterest2D[1] >= 0.0; |
| // Calculate a Rect of 1/8 the canvas, which is fixed to Rect(-1000, -1000, 1000, 1000), |
| // see https://developer.android.com/reference/android/hardware/Camera.Area.html |
| final int centerX = Math.round(pointsOfInterest2D[0] * 2000) - 1000; |
| final int centerY = Math.round(pointsOfInterest2D[1] * 2000) - 1000; |
| final int regionWidth = 2000 / 8; |
| final int regionHeight = 2000 / 8; |
| final int weight = 1000; |
| |
| mAreaOfInterest = new android.hardware.Camera.Area( |
| new Rect(Math.max(-1000, centerX - regionWidth / 2), |
| Math.max(-1000, centerY - regionHeight / 2), |
| Math.min(1000, centerX + regionWidth / 2), |
| Math.min(1000, centerY + regionHeight / 2)), |
| weight); |
| Log.d(TAG, "Area of interest %s", mAreaOfInterest.rect.toString()); |
| } |
| if (mAreaOfInterest != null) { |
| parameters.setFocusAreas(Arrays.asList(mAreaOfInterest)); |
| parameters.setMeteringAreas(Arrays.asList(mAreaOfInterest)); |
| } |
| |
| if (hasExposureCompensation) { |
| final int unnormalizedExposureCompensation = (int) Math.round( |
| exposureCompensation / parameters.getExposureCompensationStep()); |
| parameters.setExposureCompensation(unnormalizedExposureCompensation); |
| } |
| |
| // |iso| setting is not supported, see explanation in getPhotoCapabilities(). |
| |
| // White Balance mode AndroidMeteringMode.SINGLE_SHOT is not supported. |
| // TODO(mcasas): support FIXED mode, i.e. the scene mode. |
| if (whiteBalanceMode == AndroidMeteringMode.CONTINUOUS |
| && parameters.getSupportedWhiteBalance() != null) { |
| // setWhiteBalance() will release the lock set with setAutoWhiteBalanceLock(), if any. |
| parameters.setWhiteBalance(android.hardware.Camera.Parameters.WHITE_BALANCE_AUTO); |
| } else if (whiteBalanceMode == AndroidMeteringMode.FIXED |
| && parameters.isAutoWhiteBalanceLockSupported()) { |
| parameters.setAutoWhiteBalanceLock(true); |
| if (colorTemperature > 0.0) { |
| final String closestSetting = getClosestWhiteBalance((int) colorTemperature); |
| if (closestSetting != null) parameters.setWhiteBalance(closestSetting); |
| } |
| } |
| |
| if (parameters.getSupportedFlashModes() != null |
| && fillLightMode != AndroidFillLightMode.NOT_SET) { |
| switch (fillLightMode) { |
| case AndroidFillLightMode.OFF: |
| parameters.setFlashMode(android.hardware.Camera.Parameters.FLASH_MODE_OFF); |
| break; |
| case AndroidFillLightMode.AUTO: |
| parameters.setFlashMode(hasRedEyeReduction && redEyeReduction |
| ? android.hardware.Camera.Parameters.FLASH_MODE_RED_EYE |
| : android.hardware.Camera.Parameters.FLASH_MODE_AUTO); |
| break; |
| case AndroidFillLightMode.FLASH: |
| parameters.setFlashMode(android.hardware.Camera.Parameters.FLASH_MODE_ON); |
| break; |
| default: |
| } |
| if (hasTorch && torch) { |
| parameters.setFlashMode(android.hardware.Camera.Parameters.FLASH_MODE_TORCH); |
| } |
| } |
| |
| try { |
| mCamera.setParameters(parameters); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "setParameters: ", ex); |
| return; |
| } |
| |
| if (focusMode != AndroidMeteringMode.SINGLE_SHOT) return; |
| mCamera.autoFocus(new android.hardware.Camera.AutoFocusCallback() { |
| @Override |
| public void onAutoFocus(boolean success, android.hardware.Camera camera) { |
| Log.d(TAG, "onAutoFocus() finished: %s ", success ? "success" : "failed"); |
| } |
| }); |
| } |
| |
| @Override |
| public boolean takePhoto(final long callbackId) { |
| if (mCamera == null || !mIsRunning) { |
| Log.e(TAG, "takePhoto: mCamera is null or is not running"); |
| return false; |
| } |
| |
| // Only one picture can be taken at once. |
| synchronized (mPhotoTakenCallbackLock) { |
| if (mPhotoTakenCallbackId != 0) return false; |
| mPhotoTakenCallbackId = callbackId; |
| } |
| |
| android.hardware.Camera.Parameters parameters = getCameraParameters(mCamera); |
| parameters.setRotation(getCameraRotation()); |
| final android.hardware.Camera.Size original_size = parameters.getPictureSize(); |
| |
| List<android.hardware.Camera.Size> supportedSizes = parameters.getSupportedPictureSizes(); |
| android.hardware.Camera.Size closestSize = null; |
| int minDiff = Integer.MAX_VALUE; |
| for (android.hardware.Camera.Size size : supportedSizes) { |
| final int diff = ((mPhotoWidth > 0) ? Math.abs(size.width - mPhotoWidth) : 0) |
| + ((mPhotoHeight > 0) ? Math.abs(size.height - mPhotoHeight) : 0); |
| if (diff < minDiff) { |
| minDiff = diff; |
| closestSize = size; |
| } |
| } |
| Log.d(TAG, "requested resolution: (%dx%d)", mPhotoWidth, mPhotoHeight); |
| if (minDiff != Integer.MAX_VALUE) { |
| Log.d(TAG, " matched (%dx%d)", closestSize.width, closestSize.height); |
| parameters.setPictureSize(closestSize.width, closestSize.height); |
| } |
| |
| try { |
| mCamera.setParameters(parameters); |
| mCamera.takePicture(null, null, null, new CrPictureCallback()); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "takePicture ", ex); |
| return false; |
| } |
| |
| // Restore original parameters. |
| parameters.setPictureSize(original_size.width, original_size.height); |
| try { |
| mCamera.setParameters(parameters); |
| } catch (RuntimeException ex) { |
| Log.e(TAG, "takePicture ", ex); |
| return false; |
| } |
| return true; |
| } |
| |
| @Override |
| public void deallocate() { |
| if (mCamera == null) return; |
| |
| stopCapture(); |
| try { |
| mCamera.setPreviewTexture(null); |
| if (mGlTextures != null) GLES20.glDeleteTextures(1, mGlTextures, 0); |
| mCaptureFormat = null; |
| mCamera.release(); |
| mCamera = null; |
| } catch (IOException ex) { |
| Log.e(TAG, "deallocate: failed to deallocate camera, " + ex); |
| return; |
| } |
| } |
| |
| private void setPreviewCallback(android.hardware.Camera.PreviewCallback cb) { |
| mCamera.setPreviewCallbackWithBuffer(cb); |
| } |
| |
| @Override |
| public void onPreviewFrame(byte[] data, android.hardware.Camera camera) { |
| mPreviewBufferLock.lock(); |
| try { |
| if (!mIsRunning) { |
| return; |
| } |
| if (data.length == mExpectedFrameSize) { |
| nativeOnFrameAvailable(mNativeVideoCaptureDeviceAndroid, data, mExpectedFrameSize, |
| getCameraRotation()); |
| } |
| } finally { |
| mPreviewBufferLock.unlock(); |
| if (camera != null) { |
| camera.addCallbackBuffer(data); |
| } |
| } |
| } |
| } |