TestingCamera: Add preview callbacks
- Support for NV21, YV12, YUY2 preview formats
- Switch to API level 17 for certain Renderscript features
- Fix assorted SDK warnings/lint issues.
Change-Id: I3a5063065e239308df74c1077c4120429f431ab7
diff --git a/apps/TestingCamera/Android.mk b/apps/TestingCamera/Android.mk
index d7e00d1..cfe77d4 100644
--- a/apps/TestingCamera/Android.mk
+++ b/apps/TestingCamera/Android.mk
@@ -20,7 +20,11 @@
LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := 17
+
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src) \
+ $(call all-renderscript-files-under, src)
LOCAL_PACKAGE_NAME := TestingCamera
diff --git a/apps/TestingCamera/AndroidManifest.xml b/apps/TestingCamera/AndroidManifest.xml
index cfed74b..36c21ca 100644
--- a/apps/TestingCamera/AndroidManifest.xml
+++ b/apps/TestingCamera/AndroidManifest.xml
@@ -22,7 +22,7 @@
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
- <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="16"/>
+ <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="17"/>
<uses-feature android:name="android.hardware.camera.front" android:required="false"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
diff --git a/apps/TestingCamera/project.properties b/apps/TestingCamera/project.properties
index 9b84a6b..a3ee5ab 100644
--- a/apps/TestingCamera/project.properties
+++ b/apps/TestingCamera/project.properties
@@ -11,4 +11,4 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
-target=android-16
+target=android-17
diff --git a/apps/TestingCamera/res/layout/main.xml b/apps/TestingCamera/res/layout/main.xml
index cd61805..43f4cc7 100644
--- a/apps/TestingCamera/res/layout/main.xml
+++ b/apps/TestingCamera/res/layout/main.xml
@@ -25,21 +25,33 @@
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="6"
- android:orientation="vertical" >
+ android:animateLayoutChanges="false"
+ android:orientation="vertical"
+ android:visibility="visible" >
<SurfaceView
android:id="@+id/preview"
android:layout_width="fill_parent"
android:layout_height="0dp"
- android:layout_weight="6"
+ android:layout_weight="@integer/preview_only_weight"
tools:ignore="NestedWeights" />
+ <SurfaceView
+ android:id="@+id/callback_view"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:layout_weight="@integer/preview_with_callback_weight"
+ android:visibility="gone" />
+
<TextView
android:id="@+id/log"
android:layout_width="fill_parent"
- android:layout_height="0dp"
- android:layout_weight="1.5"
- android:freezesText="true" />
+ android:layout_height="10dp"
+ android:layout_weight="1"
+ android:freezesText="true"
+ android:minLines="3"
+ android:typeface="normal" />
+
</LinearLayout>
<ScrollView
@@ -198,11 +210,11 @@
android:background="@color/horiz_rule_color" />
<TextView
- android:id="@+id/textView1"
+ android:id="@+id/snapshot_size_spinner_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:text="@string/snapshot_size_spinner_label"
+ android:text="@string/snapshot_size_prompt"
android:textAppearance="?android:attr/textAppearanceSmall" />
<Spinner
@@ -287,6 +299,38 @@
android:textColorLink="@android:color/holo_blue_dark"
android:textOff="@string/record_stabilization_off_label"
android:textOn="@string/record_stabilization_on_label" />
+
+ <View
+ android:id="@+id/horizontal_rule_5"
+ android:layout_width="fill_parent"
+ android:layout_height="1dip"
+ android:layout_marginBottom="@dimen/horiz_rule_btm_margin"
+ android:layout_marginTop="@dimen/horiz_rule_top_margin"
+ android:layout_weight="1"
+ android:background="@color/horiz_rule_color" />
+
+ <TextView
+ android:id="@+id/callback_format_spinner_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/callback_format_prompt"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+ <Spinner
+ android:id="@+id/callback_format_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <ToggleButton
+ android:id="@+id/enable_callbacks"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textOff="@string/callbacks_off_label"
+ android:textOn="@string/callbacks_on_label" />
+
</LinearLayout>
</ScrollView>
diff --git a/apps/TestingCamera/res/values/dimens.xml b/apps/TestingCamera/res/values/dimens.xml
index dbea0fa..e996a32 100644
--- a/apps/TestingCamera/res/values/dimens.xml
+++ b/apps/TestingCamera/res/values/dimens.xml
@@ -2,4 +2,6 @@
<resources>
<dimen name="horiz_rule_top_margin">8dp</dimen>
<dimen name="horiz_rule_btm_margin">4dp</dimen>
+ <item format="integer" name="preview_only_weight" type="integer">6</item>
+ <item format="integer" name="preview_with_callback_weight" type="integer">3</item>
</resources>
diff --git a/apps/TestingCamera/res/values/strings.xml b/apps/TestingCamera/res/values/strings.xml
index 2225152..b8e1b85 100644
--- a/apps/TestingCamera/res/values/strings.xml
+++ b/apps/TestingCamera/res/values/strings.xml
@@ -25,7 +25,7 @@
<string name="camera_selection_prompt">Active camera</string>
<string name="default_camera_entry">No cameras found</string>
<string name="snapshot_text_default">Save to view EXIF.</string>
- <string name="snapshot_size_spinner_label">Still capture size</string>
+ <string name="snapshot_size_prompt">Still capture size</string>
<string name="record_on_label">Recording on</string>
<string name="record_off_label">Recording off</string>
<string name="record_stabilization_on_label">Video Stabilization on</string>
@@ -47,4 +47,7 @@
<string name="exposure_label">Exposure</string>
<string name="exposure_lock_on_label">Auto-Exposure Locked</string>
<string name="exposure_lock_off_label">Auto-Exposure Unlocked</string>
+ <string name="callback_format_prompt">Preview callback format</string>
+ <string name="callbacks_on_label">Callbacks on</string>
+ <string name="callbacks_off_label">Callbacks off</string>
</resources>
diff --git a/apps/TestingCamera/src/com/android/testingcamera/CallbackProcessor.java b/apps/TestingCamera/src/com/android/testingcamera/CallbackProcessor.java
new file mode 100644
index 0000000..0dfd1e3
--- /dev/null
+++ b/apps/TestingCamera/src/com/android/testingcamera/CallbackProcessor.java
@@ -0,0 +1,177 @@
+package com.android.testingcamera;
+
+import android.content.res.Resources;
+import android.graphics.ImageFormat;
+import android.graphics.PixelFormat;
+import android.os.AsyncTask;
+import android.os.SystemClock;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.Matrix4f;
+import android.renderscript.RenderScript;
+import android.renderscript.Script;
+import android.renderscript.ScriptGroup;
+import android.renderscript.ScriptIntrinsicColorMatrix;
+import android.renderscript.Type;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceView;
+
+/**
+ * Process preview callback data for display.
+ * This is done by constructing a two-step Renderscript group,
+ * the first of which converts from various YUV formats to 8bpp YUV, and
+ * the second of which converts from YUV to RGB.
+ *
+ * The processing is done in a background thread, and the result is produced
+ * into an Allocation that's backed by a SurfaceView
+ */
+class CallbackProcessor {
+ private SurfaceView mCallbackView;
+ private Surface mCallbackSurface;
+
+ private Object mTaskLock = new Object();
+
+ private RenderScript mRS;
+ private Allocation mAllocationIn;
+ private Allocation mAllocationOut;
+ private ScriptGroup mConverter;
+
+ private int mWidth;
+ private int mHeight;
+ private int mFormat;
+
+ private boolean mDone = false;
+ private boolean mTaskInProgress = false;
+
+ /**
+ * JFIF standard YCbCr <-> RGB conversion matrix,
+ * column-major order.
+ */
+ static final private float[] kJpegYuv2Rgb = new float[] {
+ 1.f, 1.f, 1.f, 0.f,
+ 0.f, -0.34414f, 1.772f, 0.f,
+ 1.402f, -0.71414f, 0.f, 0.f,
+ -0.701f, 0.529f, -0.886f, 1.0f
+ };
+
+ static final private int kStopTimeout = 2000; // ms
+
+ private static final String TAG = "CallbackProcessor";
+
+ public CallbackProcessor(int width, int height, int format,
+ Resources res, SurfaceView callbackView,
+ int viewWidth, int viewHeight,
+ RenderScript rs) {
+ mWidth = width;
+ mHeight = height;
+ mFormat = format;
+ mRS = rs;
+ mCallbackView = callbackView;
+
+ int inputSize = TestingCamera.getCallbackBufferSize(mWidth, mHeight,
+ mFormat);
+ mAllocationIn = Allocation.createSized(mRS, Element.U8(mRS), inputSize);
+
+ Type.Builder tb = new Type.Builder(mRS, Element.RGBA_8888(mRS));
+ tb.setX(viewWidth);
+ tb.setY(viewHeight);
+ Type outType = tb.create();
+
+ mAllocationOut = Allocation.createTyped(mRS, outType,
+ Allocation.USAGE_IO_OUTPUT | Allocation.USAGE_SCRIPT);
+
+ ScriptC_callback swizzleScript =
+ new ScriptC_callback(mRS, res, R.raw.callback);
+ swizzleScript.bind_yuv_in(mAllocationIn);
+ swizzleScript.invoke_init_convert(mWidth, mHeight,
+ mFormat, viewWidth, viewHeight);
+ Script.KernelID swizzleId;
+ switch (mFormat) {
+ case ImageFormat.NV21:
+ swizzleId = swizzleScript.getKernelID_convert_semiplanar();
+ break;
+ case ImageFormat.YV12:
+ swizzleId = swizzleScript.getKernelID_convert_planar();
+ break;
+ case ImageFormat.YUY2:
+ swizzleId = swizzleScript.getKernelID_convert_interleaved();
+ break;
+ case ImageFormat.UNKNOWN:
+ default:
+ swizzleId = swizzleScript.getKernelID_convert_unknown();
+ }
+
+ ScriptIntrinsicColorMatrix colorMatrix =
+ ScriptIntrinsicColorMatrix.create(rs, Element.U8_4(mRS));
+
+ Matrix4f yuv2rgb = new Matrix4f(kJpegYuv2Rgb);
+ colorMatrix.setColorMatrix(yuv2rgb);
+
+ ScriptGroup.Builder b = new ScriptGroup.Builder(rs);
+ b.addKernel(swizzleId);
+ b.addKernel(colorMatrix.getKernelID());
+ b.addConnection(outType, swizzleId,
+ colorMatrix.getKernelID());
+ mConverter = b.create();
+
+ mConverter.setOutput(colorMatrix.getKernelID(), mAllocationOut);
+ }
+
+ public boolean stop() {
+ synchronized(mTaskLock) {
+ mDone = true;
+ long startTime = SystemClock.elapsedRealtime();
+ while (mTaskInProgress) {
+ try {
+ mTaskLock.wait(kStopTimeout);
+ } catch (InterruptedException e) {
+ // ignored, keep waiting
+ }
+ long endTime = SystemClock.elapsedRealtime();
+ if (endTime - startTime > kStopTimeout) {
+ return false;
+ }
+ }
+ }
+ mAllocationOut.setSurface(null);
+ return true;
+ }
+
+ public void displayCallback(byte[] data) {
+ synchronized(mTaskLock) {
+ if (mTaskInProgress || mDone) return;
+ mTaskInProgress = true;
+ }
+ if (mCallbackSurface == null) {
+ mCallbackView.getHolder().setFormat(PixelFormat.RGBA_8888);
+ mCallbackSurface = mCallbackView.getHolder().getSurface();
+ if (mCallbackSurface == null) return;
+ mAllocationOut.setSurface(mCallbackSurface);
+ }
+ new ProcessCallbackTask().execute(data);
+ }
+
+ private class ProcessCallbackTask extends AsyncTask<byte[], Void, Boolean> {
+
+ @Override
+ protected Boolean doInBackground(byte[]... datas) {
+ byte[] data = datas[0];
+
+ mAllocationIn.copyFrom(data);
+ mConverter.execute();
+ mAllocationOut.ioSend();
+
+ synchronized(mTaskLock) {
+ mTaskInProgress = false;
+ mTaskLock.notify();
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ }
+ }
+
+}
diff --git a/apps/TestingCamera/src/com/android/testingcamera/InfoDialogFragment.java b/apps/TestingCamera/src/com/android/testingcamera/InfoDialogFragment.java
index da5496a..9499778 100644
--- a/apps/TestingCamera/src/com/android/testingcamera/InfoDialogFragment.java
+++ b/apps/TestingCamera/src/com/android/testingcamera/InfoDialogFragment.java
@@ -37,6 +37,7 @@
return view;
}
+ @Override
public void onClick(View v) {
this.dismiss();
}
diff --git a/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java b/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
index d22f751..6995320 100644
--- a/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
+++ b/apps/TestingCamera/src/com/android/testingcamera/SnapshotDialogFragment.java
@@ -22,7 +22,7 @@
import android.widget.ImageView;
import android.widget.TextView;
-public class SnapshotDialogFragment extends DialogFragment
+class SnapshotDialogFragment extends DialogFragment
implements OnScanCompletedListener{
private ImageView mInfoImage;
@@ -81,6 +81,7 @@
return img;
}
+ @Override
protected void onPostExecute(Bitmap img) {
mInfoImage.setImageBitmap(img);
}
@@ -92,18 +93,21 @@
}
public OnClickListener mOkButtonListener = new OnClickListener() {
+ @Override
public void onClick(View v) {
dismiss();
}
};
public OnClickListener mSaveButtonListener = new OnClickListener() {
+ @Override
public void onClick(View v) {
saveFile();
}
};
public OnClickListener mSaveAndViewButtonListener = new OnClickListener() {
+ @Override
public void onClick(View v) {
saveFile();
viewFile();
@@ -246,6 +250,7 @@
}
}
+ @Override
public synchronized void onScanCompleted(String path, Uri uri) {
mSavedUri = uri;
if (mViewWhenReady) viewFile();
diff --git a/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java b/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
index d24b3f3..a60b188 100644
--- a/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
+++ b/apps/TestingCamera/src/com/android/testingcamera/TestingCamera.java
@@ -16,8 +16,11 @@
package com.android.testingcamera;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.FragmentManager;
+import android.content.res.Resources;
+import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.media.CamcorderProfile;
@@ -27,6 +30,7 @@
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
+import android.os.SystemClock;
import android.view.View;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@@ -35,13 +39,16 @@
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
+import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.ToggleButton;
+import android.renderscript.RenderScript;
import android.text.Layout;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
+import android.util.SparseArray;
import java.io.File;
import java.io.IOException;
@@ -60,11 +67,16 @@
* The goal of this application is to allow all camera API features to be
* exercised, and all information provided by the API to be shown.
*/
-public class TestingCamera extends Activity implements SurfaceHolder.Callback {
+public class TestingCamera extends Activity
+ implements SurfaceHolder.Callback, Camera.PreviewCallback {
/** UI elements */
private SurfaceView mPreviewView;
private SurfaceHolder mPreviewHolder;
+ private LinearLayout mPreviewColumn;
+
+ private SurfaceView mCallbackView;
+ private SurfaceHolder mCallbackHolder;
private Spinner mCameraSpinner;
private Button mInfoButton;
@@ -82,17 +94,23 @@
private Spinner mVideoFrameRateSpinner;
private ToggleButton mRecordToggle;
private ToggleButton mRecordStabilizationToggle;
+ private Spinner mCallbackFormatSpinner;
+ private ToggleButton mCallbackToggle;
private TextView mLogView;
private Set<View> mPreviewOnlyControls = new HashSet<View>();
+ private SparseArray<String> mFormatNames;
+
/** Camera state */
private int mCameraId = 0;
private Camera mCamera;
private Camera.Parameters mParams;
private List<Camera.Size> mPreviewSizes;
private int mPreviewSize = 0;
+ private List<Integer> mPreviewFormats;
+ private int mPreviewFormat = 0;
private List<String> mAfModes;
private int mAfMode = 0;
private List<String> mFlashModes;
@@ -109,6 +127,18 @@
private MediaRecorder mRecorder;
private File mRecordingFile;
+ private RenderScript mRS;
+
+ private boolean mCallbacksEnabled = false;
+ private CallbackProcessor mCallbackProcessor = null;
+ long mLastCallbackTimestamp = -1;
+ float mCallbackAvgFrameDuration = 30;
+ int mCallbackFrameCount = 0;
+ private static final float MEAN_FPS_HISTORY_COEFF = 0.9f;
+ private static final float MEAN_FPS_MEASUREMENT_COEFF = 0.1f;
+ private static final int FPS_REPORTING_PERIOD = 200; // frames
+ private static final int CALLBACK_BUFFER_COUNT = 3;
+
private static final int CAMERA_UNINITIALIZED = 0;
private static final int CAMERA_OPEN = 1;
private static final int CAMERA_PREVIEW = 2;
@@ -116,6 +146,8 @@
private static final int CAMERA_RECORD = 4;
private int mState = CAMERA_UNINITIALIZED;
+
+
/** Misc variables */
private static final String TAG = "TestingCamera";
@@ -128,9 +160,13 @@
setContentView(R.layout.main);
- mPreviewView = (SurfaceView)findViewById(R.id.preview);
+ mPreviewColumn = (LinearLayout) findViewById(R.id.preview_column);
+
+ mPreviewView = (SurfaceView) findViewById(R.id.preview);
mPreviewView.getHolder().addCallback(this);
+ mCallbackView = (SurfaceView)findViewById(R.id.callback_view);
+
mCameraSpinner = (Spinner) findViewById(R.id.camera_spinner);
mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
@@ -183,9 +219,24 @@
mRecordStabilizationToggle = (ToggleButton) findViewById(R.id.record_stabilization);
mRecordStabilizationToggle.setOnClickListener(mRecordStabilizationToggleListener);
+ mCallbackFormatSpinner = (Spinner) findViewById(R.id.callback_format_spinner);
+ mCallbackFormatSpinner.setOnItemSelectedListener(mCallbackFormatListener);
+
+ mCallbackToggle = (ToggleButton) findViewById(R.id.enable_callbacks);
+ mCallbackToggle.setOnClickListener(mCallbackToggleListener);
+
mLogView = (TextView) findViewById(R.id.log);
mLogView.setMovementMethod(new ScrollingMovementMethod());
+ mFormatNames = new SparseArray<String>(7);
+ mFormatNames.append(ImageFormat.JPEG, "JPEG");
+ mFormatNames.append(ImageFormat.NV16, "NV16");
+ mFormatNames.append(ImageFormat.NV21, "NV21");
+ mFormatNames.append(ImageFormat.RGB_565, "RGB_565");
+ mFormatNames.append(ImageFormat.UNKNOWN, "UNKNOWN");
+ mFormatNames.append(ImageFormat.YUY2, "YUY2");
+ mFormatNames.append(ImageFormat.YV12, "YV12");
+
int numCameras = Camera.getNumberOfCameras();
String[] cameraNames = new String[numCameras];
for (int i = 0; i < numCameras; i++) {
@@ -195,6 +246,8 @@
mCameraSpinner.setAdapter(
new ArrayAdapter<String>(this,
R.layout.spinner_item, cameraNames));
+
+ mRS = RenderScript.create(this);
}
@Override
@@ -214,25 +267,53 @@
}
/** SurfaceHolder.Callback methods */
+ @Override
public void surfaceChanged(SurfaceHolder holder,
int format,
int width,
int height) {
- if (mPreviewHolder != null) return;
+ if (holder == mPreviewView.getHolder()) {
+ if (mState >= CAMERA_OPEN) {
+ final int previewWidth =
+ mPreviewSizes.get(mPreviewSize).width;
+ final int previewHeight =
+ mPreviewSizes.get(mPreviewSize).height;
- log("Surface holder available: " + width + " x " + height);
- mPreviewHolder = holder;
- try {
- mCamera.setPreviewDisplay(holder);
- } catch (IOException e) {
- logE("Unable to set up preview!");
+ if ( Math.abs((float)previewWidth / previewHeight -
+ (float)width/height) > 0.01f) {
+ Handler h = new Handler();
+ h.post(new Runnable() {
+ @Override
+ public void run() {
+ layoutPreview();
+ }
+ });
+ }
+ }
+
+ if (mPreviewHolder != null) {
+ return;
+ }
+ log("Surface holder available: " + width + " x " + height);
+ mPreviewHolder = holder;
+ try {
+ if (mCamera != null) {
+ mCamera.setPreviewDisplay(holder);
+ }
+ } catch (IOException e) {
+ logE("Unable to set up preview!");
+ }
+ } else if (holder == mCallbackView.getHolder()) {
+ mCallbackHolder = holder;
}
}
+ @Override
public void surfaceCreated(SurfaceHolder holder) {
}
+ @Override
public void surfaceDestroyed(SurfaceHolder holder) {
mPreviewHolder = null;
}
@@ -248,6 +329,7 @@
private AdapterView.OnItemSelectedListener mCameraSpinnerListener =
new AdapterView.OnItemSelectedListener() {
+ @Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (mCameraId != pos) {
@@ -256,12 +338,14 @@
}
}
+ @Override
public void onNothingSelected(AdapterView<?> parent) {
}
};
private OnClickListener mInfoButtonListener = new OnClickListener() {
+ @Override
public void onClick(View v) {
FragmentManager fm = getFragmentManager();
InfoDialogFragment infoDialog = new InfoDialogFragment();
@@ -272,11 +356,13 @@
private AdapterView.OnItemSelectedListener mPreviewSizeListener =
new AdapterView.OnItemSelectedListener() {
+ @Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (pos == mPreviewSize) return;
if (mState == CAMERA_PREVIEW) {
- log("Stopping preview to switch resolutions");
+ log("Stopping preview and callbacks to switch resolutions");
+ stopCallbacks();
mCamera.stopPreview();
}
@@ -288,14 +374,15 @@
log("Setting preview size to " + width + "x" + height);
mCamera.setParameters(mParams);
+ resizePreview();
if (mState == CAMERA_PREVIEW) {
log("Restarting preview");
- resizePreview(width, height);
mCamera.startPreview();
}
}
+ @Override
public void onNothingSelected(AdapterView<?> parent) {
}
@@ -303,6 +390,7 @@
private View.OnClickListener mPreviewToggleListener =
new View.OnClickListener() {
+ @Override
public void onClick(View v) {
if (mState == CAMERA_TAKE_PICTURE) {
logE("Can't change preview state while taking picture!");
@@ -310,8 +398,6 @@
}
if (mPreviewToggle.isChecked()) {
log("Starting preview");
- resizePreview(mPreviewSizes.get(mPreviewSize).width,
- mPreviewSizes.get(mPreviewSize).height);
mCamera.startPreview();
mState = CAMERA_PREVIEW;
enablePreviewOnlyControls(true);
@@ -327,6 +413,7 @@
private OnItemSelectedListener mAutofocusModeListener =
new OnItemSelectedListener() {
+ @Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (pos == mAfMode) return;
@@ -343,6 +430,7 @@
mCamera.setParameters(mParams);
}
+ @Override
public void onNothingSelected(AdapterView<?> arg0) {
}
@@ -350,6 +438,7 @@
private OnClickListener mAutofocusButtonListener =
new View.OnClickListener() {
+ @Override
public void onClick(View v) {
log("Triggering autofocus");
mCamera.autoFocus(mAutofocusCallback);
@@ -358,6 +447,7 @@
private OnClickListener mCancelAutofocusButtonListener =
new View.OnClickListener() {
+ @Override
public void onClick(View v) {
log("Cancelling autofocus");
mCamera.cancelAutoFocus();
@@ -366,6 +456,7 @@
private Camera.AutoFocusCallback mAutofocusCallback =
new Camera.AutoFocusCallback() {
+ @Override
public void onAutoFocus(boolean success, Camera camera) {
log("Autofocus completed: " + (success ? "success" : "failure") );
}
@@ -373,6 +464,7 @@
private Camera.AutoFocusMoveCallback mAutofocusMoveCallback =
new Camera.AutoFocusMoveCallback() {
+ @Override
public void onAutoFocusMoving(boolean start, Camera camera) {
log("Autofocus movement: " + (start ? "starting" : "stopped") );
}
@@ -380,6 +472,7 @@
private OnItemSelectedListener mFlashModeListener =
new OnItemSelectedListener() {
+ @Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (pos == mFlashMode) return;
@@ -391,6 +484,7 @@
mCamera.setParameters(mParams);
}
+ @Override
public void onNothingSelected(AdapterView<?> arg0) {
}
@@ -399,6 +493,7 @@
private AdapterView.OnItemSelectedListener mSnapshotSizeListener =
new AdapterView.OnItemSelectedListener() {
+ @Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (pos == mSnapshotSize) return;
@@ -413,6 +508,7 @@
mCamera.setParameters(mParams);
}
+ @Override
public void onNothingSelected(AdapterView<?> parent) {
}
@@ -420,6 +516,7 @@
private View.OnClickListener mTakePictureListener =
new View.OnClickListener() {
+ @Override
public void onClick(View v) {
log("Taking picture");
if (mState == CAMERA_PREVIEW) {
@@ -436,6 +533,7 @@
private AdapterView.OnItemSelectedListener mCamcorderProfileListener =
new AdapterView.OnItemSelectedListener() {
+ @Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (pos != mCamcorderProfile) {
@@ -458,6 +556,7 @@
mVideoRecordSizeSpinner.setSelection(mVideoRecordSize);
}
+ @Override
public void onNothingSelected(AdapterView<?> parent) {
}
@@ -465,6 +564,7 @@
private AdapterView.OnItemSelectedListener mVideoRecordSizeListener =
new AdapterView.OnItemSelectedListener() {
+ @Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (pos == mVideoRecordSize) return;
@@ -473,6 +573,7 @@
mVideoRecordSize = pos;
}
+ @Override
public void onNothingSelected(AdapterView<?> parent) {
}
@@ -480,6 +581,7 @@
private AdapterView.OnItemSelectedListener mVideoFrameRateListener =
new AdapterView.OnItemSelectedListener() {
+ @Override
public void onItemSelected(AdapterView<?> parent,
View view, int pos, long id) {
if (pos == mVideoFrameRate) return;
@@ -488,6 +590,7 @@
mVideoFrameRate = pos;
}
+ @Override
public void onNothingSelected(AdapterView<?> parent) {
}
@@ -495,6 +598,7 @@
private View.OnClickListener mRecordToggleListener =
new View.OnClickListener() {
+ @Override
public void onClick(View v) {
mPreviewToggle.setEnabled(false);
if (mState == CAMERA_PREVIEW) {
@@ -510,31 +614,38 @@
private View.OnClickListener mRecordStabilizationToggleListener =
new View.OnClickListener() {
+ @Override
public void onClick(View v) {
boolean on = ((ToggleButton) v).isChecked();
mParams.setVideoStabilization(on);
+
+ mCamera.setParameters(mParams);
}
};
private Camera.ShutterCallback mShutterCb = new Camera.ShutterCallback() {
+ @Override
public void onShutter() {
log("Shutter callback received");
}
};
private Camera.PictureCallback mRawCb = new Camera.PictureCallback() {
+ @Override
public void onPictureTaken(byte[] data, Camera camera) {
log("Raw callback received");
}
};
private Camera.PictureCallback mPostviewCb = new Camera.PictureCallback() {
+ @Override
public void onPictureTaken(byte[] data, Camera camera) {
log("Postview callback received");
}
};
private Camera.PictureCallback mJpegCb = new Camera.PictureCallback() {
+ @Override
public void onPictureTaken(byte[] data, Camera camera) {
log("JPEG picture callback received");
FragmentManager fm = getFragmentManager();
@@ -549,6 +660,70 @@
}
};
+ private AdapterView.OnItemSelectedListener mCallbackFormatListener =
+ new AdapterView.OnItemSelectedListener() {
+ public void onItemSelected(AdapterView<?> parent,
+ View view, int pos, long id) {
+ mPreviewFormat = pos;
+
+ log("Setting preview format to " +
+ mFormatNames.get(mPreviewFormats.get(mPreviewFormat)));
+
+ switch (mState) {
+ case CAMERA_UNINITIALIZED:
+ return;
+ case CAMERA_OPEN:
+ break;
+ case CAMERA_PREVIEW:
+ if (mCallbacksEnabled) {
+ log("Stopping preview and callbacks to switch formats");
+ stopCallbacks();
+ mCamera.stopPreview();
+ }
+ break;
+ case CAMERA_RECORD:
+ logE("Can't update format while recording active");
+ return;
+ }
+
+ mParams.setPreviewFormat(mPreviewFormats.get(mPreviewFormat));
+
+ if (mCallbacksEnabled) {
+ mCamera.setParameters(mParams);
+
+ if (mState == CAMERA_PREVIEW) {
+ mCamera.startPreview();
+ }
+ }
+
+ configureCallbacks(mCallbackView.getWidth(), mCallbackView.getHeight());
+ }
+
+ public void onNothingSelected(AdapterView<?> parent) {
+
+ }
+ };
+
+ private View.OnClickListener mCallbackToggleListener =
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ if (mCallbacksEnabled) {
+ log("Disabling preview callbacks");
+ stopCallbacks();
+ mCallbacksEnabled = false;
+ resizePreview();
+ mCallbackView.setVisibility(View.GONE);
+
+ } else {
+ log("Enabling preview callbacks");
+ mCallbacksEnabled = true;
+ resizePreview();
+ mCallbackView.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+
+
// Internal methods
void setUpCamera() {
@@ -571,6 +746,7 @@
logIndent(1);
updatePreviewSizes(mParams);
+ updatePreviewFormats(mParams);
updateAfModes(mParams);
updateFlashModes(mParams);
updateSnapshotSizes(mParams);
@@ -615,9 +791,7 @@
mPreviewToggle.setChecked(false);
enablePreviewOnlyControls(false);
- int width = mPreviewSizes.get(mPreviewSize).width;
- int height = mPreviewSizes.get(mPreviewSize).height;
- resizePreview(width, height);
+ resizePreview();
if (mPreviewToggle.isChecked()) {
log("Starting preview" );
mCamera.startPreview();
@@ -688,6 +862,28 @@
log("Setting preview size to " + width + " x " + height);
}
+ private void updatePreviewFormats(Camera.Parameters params) {
+ mPreviewFormats = params.getSupportedPreviewFormats();
+
+ String[] availableFormatNames = new String[mPreviewFormats.size()];
+ int i = 0;
+ for (Integer previewFormat: mPreviewFormats) {
+ availableFormatNames[i++] = mFormatNames.get(previewFormat);
+ }
+ mCallbackFormatSpinner.setAdapter(
+ new ArrayAdapter<String>(
+ this, R.layout.spinner_item, availableFormatNames));
+
+ mPreviewFormat = 0;
+ mCallbacksEnabled = false;
+ mCallbackToggle.setChecked(false);
+ mCallbackView.setVisibility(View.GONE);
+
+ params.setPreviewFormat(mPreviewFormats.get(mPreviewFormat));
+ log("Setting preview format to " +
+ mFormatNames.get(mPreviewFormats.get(mPreviewFormat)));
+ }
+
private void updateSnapshotSizes(Camera.Parameters params) {
String[] availableSizeNames;
mSnapshotSizes = params.getSupportedPictureSizes();
@@ -816,22 +1012,114 @@
this, R.layout.spinner_item, nameArray));
mVideoFrameRate = 0;
- log("Setting frame rate to " + nameArray[mVideoFrameRate]);
+ log("Setting recording frame rate to " + nameArray[mVideoFrameRate]);
}
- void resizePreview(int width, int height) {
- if (mPreviewHolder != null) {
- int viewHeight = mPreviewView.getHeight();
- int viewWidth = (int)(((double)width)/height * viewHeight);
+ void resizePreview() {
+ // Reset preview layout parameters, to trigger layout pass
+ // This will eventually call layoutPreview below
+ Resources res = getResources();
+ mPreviewView.setLayoutParams(
+ new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0,
+ mCallbacksEnabled ?
+ res.getInteger(R.integer.preview_with_callback_weight):
+ res.getInteger(R.integer.preview_only_weight) ));
+ }
- mPreviewView.setLayoutParams(
- new LayoutParams(viewWidth, viewHeight));
+ void layoutPreview() {
+ int width = mPreviewSizes.get(mPreviewSize).width;
+ int height = mPreviewSizes.get(mPreviewSize).height;
+ float previewAspect = ((float) width) / height;
+
+ int viewHeight = mPreviewView.getHeight();
+ int viewWidth = mPreviewView.getWidth();
+ float viewAspect = ((float) viewWidth) / viewHeight;
+ if ( previewAspect > viewAspect) {
+ viewHeight = (int) (viewWidth / previewAspect);
+ } else {
+ viewWidth = (int) (viewHeight * previewAspect);
}
+ mPreviewView.setLayoutParams(
+ new LayoutParams(viewWidth, viewHeight));
+ if (mCallbacksEnabled) {
+ int callbackHeight = mCallbackView.getHeight();
+ int callbackWidth = mCallbackView.getWidth();
+ float callbackAspect = ((float) callbackWidth) / callbackHeight;
+ if ( previewAspect > callbackAspect) {
+ callbackHeight = (int) (callbackWidth / previewAspect);
+ } else {
+ callbackWidth = (int) (callbackHeight * previewAspect);
+ }
+ mCallbackView.setLayoutParams(
+ new LayoutParams(callbackWidth, callbackHeight));
+ configureCallbacks(callbackWidth, callbackHeight);
+ }
+ }
+
+
+ private void configureCallbacks(int callbackWidth, int callbackHeight) {
+ if (mState >= CAMERA_OPEN && mCallbacksEnabled) {
+ mCamera.setPreviewCallbackWithBuffer(null);
+ int width = mPreviewSizes.get(mPreviewSize).width;
+ int height = mPreviewSizes.get(mPreviewSize).height;
+ int format = mPreviewFormats.get(mPreviewFormat);
+
+ mCallbackProcessor = new CallbackProcessor(width, height, format,
+ getResources(), mCallbackView,
+ callbackWidth, callbackHeight, mRS);
+
+ int size = getCallbackBufferSize(width, height, format);
+ log("Configuring callbacks:" + width + " x " + height +
+ " , format " + format);
+ for (int i = 0; i < CALLBACK_BUFFER_COUNT; i++) {
+ mCamera.addCallbackBuffer(new byte[size]);
+ }
+ mCamera.setPreviewCallbackWithBuffer(this);
+ }
+ mLastCallbackTimestamp = -1;
+ mCallbackFrameCount = 0;
+ mCallbackAvgFrameDuration = 30;
+ }
+
+ private void stopCallbacks() {
+ if (mState >= CAMERA_OPEN) {
+ mCamera.setPreviewCallbackWithBuffer(null);
+ if (mCallbackProcessor != null) {
+ if (!mCallbackProcessor.stop()) {
+ logE("Can't stop preview callback processing!");
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ long timestamp = SystemClock.elapsedRealtime();
+ if (mLastCallbackTimestamp != -1) {
+ long frameDuration = timestamp - mLastCallbackTimestamp;
+ mCallbackAvgFrameDuration =
+ mCallbackAvgFrameDuration * MEAN_FPS_HISTORY_COEFF +
+ frameDuration * MEAN_FPS_MEASUREMENT_COEFF;
+ }
+ mLastCallbackTimestamp = timestamp;
+ if (mState < CAMERA_PREVIEW || !mCallbacksEnabled) {
+ mCamera.addCallbackBuffer(data);
+ return;
+ }
+ mCallbackFrameCount++;
+ if (mCallbackFrameCount % FPS_REPORTING_PERIOD == 0) {
+ log("Got " + FPS_REPORTING_PERIOD + " callback frames, fps "
+ + 1e3/mCallbackAvgFrameDuration);
+ }
+ mCallbackProcessor.displayCallback(data);
+
+ mCamera.addCallbackBuffer(data);
}
static final int MEDIA_TYPE_IMAGE = 0;
static final int MEDIA_TYPE_VIDEO = 1;
+ @SuppressLint("SimpleDateFormat")
File getOutputMediaFile(int type){
// To be safe, you should check that the SDCard is mounted
// using Environment.getExternalStorageState() before doing this.
@@ -877,8 +1165,10 @@
new String[] { newFile.toString() },
null,
new MediaScannerConnection.OnScanCompletedListener() {
+ @Override
public void onScanCompleted(final String path, final Uri uri) {
h.post(new Runnable() {
+ @Override
public void run() {
log("MediaScanner notified: " +
path + " -> " + uri);
@@ -955,6 +1245,7 @@
private MediaRecorder.OnErrorListener mRecordingErrorListener =
new MediaRecorder.OnErrorListener() {
+ @Override
public void onError(MediaRecorder mr, int what, int extra) {
logE("MediaRecorder reports error: " + what + ", extra "
+ extra);
@@ -966,6 +1257,7 @@
private MediaRecorder.OnInfoListener mRecordingInfoListener =
new MediaRecorder.OnInfoListener() {
+ @Override
public void onInfo(MediaRecorder mr, int what, int extra) {
log("MediaRecorder reports info: " + what + ", extra "
+ extra);
@@ -989,6 +1281,36 @@
}
}
+ static int getCallbackBufferSize(int width, int height, int format) {
+ int size = -1;
+ switch (format) {
+ case ImageFormat.NV21:
+ size = width * height * 3 / 2;
+ break;
+ case ImageFormat.YV12:
+ int y_stride = (int) (Math.ceil( width / 16.) * 16);
+ int y_size = y_stride * height;
+ int c_stride = (int) (Math.ceil(y_stride / 32.) * 16);
+ int c_size = c_stride * height/2;
+ size = y_size + c_size * 2;
+ break;
+ case ImageFormat.NV16:
+ case ImageFormat.RGB_565:
+ case ImageFormat.YUY2:
+ size = 2 * width * height;
+ break;
+ case ImageFormat.JPEG:
+ Log.e(TAG, "JPEG callback buffers not supported!");
+ size = 0;
+ break;
+ case ImageFormat.UNKNOWN:
+ Log.e(TAG, "Unknown-format callback buffers not supported!");
+ size = 0;
+ break;
+ }
+ return size;
+ }
+
private int mLogIndentLevel = 0;
private String mLogIndent = "\t";
/** Increment or decrement log indentation level */
@@ -1002,6 +1324,7 @@
mLogIndent = new String(mLogIndentArray);
}
+ @SuppressLint("SimpleDateFormat")
SimpleDateFormat mDateFormatter = new SimpleDateFormat("HH:mm:ss.SSS");
/** Log both to log text view and to device logcat */
void log(String logLine) {
diff --git a/apps/TestingCamera/src/com/android/testingcamera/callback.rs b/apps/TestingCamera/src/com/android/testingcamera/callback.rs
new file mode 100644
index 0000000..ba7b891
--- /dev/null
+++ b/apps/TestingCamera/src/com/android/testingcamera/callback.rs
@@ -0,0 +1,132 @@
+#pragma version(1)
+#pragma rs java_package_name(com.android.testingcamera)
+#pragma rs_fp_relaxed
+
+uchar *yuv_in;
+
+// Input globals
+uint32_t yuv_height;
+uint32_t yuv_width;
+uint32_t out_width;
+uint32_t out_height;
+// Derived globals
+uint32_t y_stride;
+uint32_t uv_stride;
+uint32_t u_start;
+uint32_t v_start;
+float x_scale;
+float y_scale;
+
+enum ImageFormat {
+ NV16 = 16,
+ NV21 = 17,
+ RGB_565 = 4,
+ UNKNOWN = 0,
+ YUY2 = 20,
+ YV12 = 0x32315659
+};
+
+// Must be called before using any conversion methods
+void init_convert(uint32_t yw, uint32_t yh, uint32_t format,
+ uint32_t ow, uint32_t oh) {
+ yuv_height = yh;
+ yuv_width = yw;
+ out_width = ow;
+ out_height = oh;
+
+ x_scale = (float)yuv_width / out_width;
+ y_scale = (float)yuv_height / out_height;
+
+ switch (format) {
+ case NV16:
+ case NV21:
+ y_stride = yuv_width;
+ uv_stride = yuv_width;
+ v_start = y_stride * yuv_height;
+ u_start = v_start + 1;
+ break;
+ case YV12:
+ // Minimum align-16 stride
+ y_stride = (yuv_width + 0xF) & ~0xF;
+ uv_stride = (y_stride / 2 + 0xF) & ~0xF;
+ v_start = y_stride * yuv_height;
+ u_start = v_start + uv_stride * (yuv_height / 2);
+ break;
+ case YUY2:
+ y_stride = yuv_width * 2;
+ uv_stride = y_stride;
+ u_start = 1;
+ v_start = 3;
+ break;
+ case RGB_565:
+ case UNKNOWN:
+ default:
+ y_stride = yuv_width;
+ uv_stride = yuv_width;
+ v_start = 0;
+ u_start = 0;
+ }
+}
+
+// Makes up a conversion for unknown YUV types to try to display something
+// Asssumes that there's at least 1bpp in input YUV data
+uchar4 __attribute__((kernel)) convert_unknown(uint32_t x, uint32_t y) {
+ uint32_t x_scaled = x * x_scale;
+ uint32_t y_scaled = y * y_scale;
+
+ uchar4 out;
+ out.r = yuv_in[y_stride * y_scaled + x_scaled];
+ out.g = 128;
+ out.b = 128;
+ out.a = 255; // For affine transform later
+ return out;
+}
+
+// Converts semiplanar YVU to interleaved YUV, nearest neighbor
+uchar4 __attribute__((kernel)) convert_semiplanar(uint32_t x, uint32_t y) {
+ uint32_t x_scaled = x * x_scale;
+ uint32_t y_scaled = y * y_scale;
+
+ uint32_t uv_row = y_scaled / 2; // truncation is important here
+ uint32_t uv_col = x_scaled & ~0x1;
+ uint32_t vu_pixel = uv_row * uv_stride + uv_col;
+
+ uchar4 out;
+ out.r = yuv_in[y_stride * y_scaled + x_scaled];
+ out.g = yuv_in[u_start + vu_pixel];
+ out.b = yuv_in[v_start + vu_pixel];
+ out.a = 255; // For affine transform later
+ return out;
+}
+
+// Converts planar YVU to interleaved YUV, nearest neighbor
+uchar4 __attribute__((kernel)) convert_planar(uint32_t x, uint32_t y) {
+ uint32_t x_scaled = x * x_scale;
+ uint32_t y_scaled = y * y_scale;
+
+ uint32_t uv_row = y_scaled / 2; // truncation is important here
+ uint32_t vu_pixel = uv_stride * uv_row + x_scaled / 2;
+
+ uchar4 out;
+ out.r = yuv_in[y_stride * y_scaled + x_scaled];
+ out.g = yuv_in[u_start + vu_pixel];
+ out.b = yuv_in[v_start + vu_pixel];
+ out.a = 255; // For affine transform later
+ return out;
+}
+
+// Converts interleaved 4:2:2 YUV to interleaved YUV, nearest neighbor
+uchar4 __attribute__((kernel)) convert_interleaved(uint32_t x, uint32_t y) {
+ uint32_t x_scaled = x * x_scale;
+ uint32_t y_scaled = y * y_scale;
+
+ uint32_t uv_col = 2 * (x_scaled & ~0x1);
+ uint32_t vu_pixel = y_stride * y_scaled + uv_col;
+
+ uchar4 out;
+ out.r = yuv_in[y_stride * y_scaled + x_scaled * 2];
+ out.g = yuv_in[u_start + vu_pixel];
+ out.b = yuv_in[v_start + vu_pixel];
+ out.a = 255; // For affine transform later
+ return out;
+}