diff --git a/tests/Camera2Tests/SmartCamera/Android.mk b/tests/Camera2Tests/SmartCamera/Android.mk
new file mode 100644
index 0000000000000..3fa8f54ae030b
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/Android.mk
@@ -0,0 +1,14 @@
+# Copyright 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+include $(call all-subdir-makefiles)
diff --git a/tests/Camera2Tests/SmartCamera/README.txt b/tests/Camera2Tests/SmartCamera/README.txt
new file mode 100644
index 0000000000000..1fff3ab8f9149
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/README.txt
@@ -0,0 +1,60 @@
+Copyright 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+Smart Camera / Auto Snapshot (formerly named SimpleCamera) ReadMe
+
+Created by: Benjamin W Hendricks
+
+How to build the application:
+From root: make SmartCamera will build the apk for generic
+Otherwise, to build the application for a specific device, lunch to that device
+and then run mm while in the SimpleCamera directory.
+Then take the given Install path (out/target/.../SmartCamera.apk)
+and run adb install out/target/.../SmartCamera.apk. The application should
+then appear in the launcher of your device.
+You might also need to run adb sync after building to sync the
+libsmartcamera_jni library
+Summarized:
+ make SmartCamera
+ adb remount
+ adb sync
+ adb install -r $ANDROID_PRODUCT_OUT/data/app/SmartCamera.apk
+
+How to run the application:
+On a Nexus 7, open up the application from the launcher, and the camera preview
+should appear. From there, you can go to the gallery with the gallery button or
+press start to start capturing images. You can also change the number of images
+to be captured by changing the number on the spinner (between 1-10).
+
+What does it do:
+The application tries to take good pictures for you automatically when in the
+start mode. On stop, the application will capture whatever images are in the
+bottom preview and save them to the Gallery. It does this by looking at the
+following image features:
+ - Sharpness
+ - Brightness
+ - Motion of the device
+ - Colorfulness
+ - Contrast
+ - Exposure (over/under)
+
+By comparing each of these features frame by frame, a score is calculated to
+determine whether an image is better or worse than the previous few frames,
+and from that score I can determine the great images from the bad ones.
+
+What libraries does it use:
+- Mobile Filter Framework (MFF)
+- Camera2 API
+- Renderscript
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/.classpath b/tests/Camera2Tests/SmartCamera/SimpleCamera/.classpath
new file mode 100644
index 0000000000000..3f9691c5dda29
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/.classpath
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/.project b/tests/Camera2Tests/SmartCamera/SimpleCamera/.project
new file mode 100644
index 0000000000000..2517e2d4f93d1
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/.project
@@ -0,0 +1,33 @@
+
+
+ CameraShoot
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/Android.mk b/tests/Camera2Tests/SmartCamera/SimpleCamera/Android.mk
new file mode 100644
index 0000000000000..ed0c29417c6df
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/Android.mk
@@ -0,0 +1,40 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ifneq ($(TARGET_BUILD_JAVA_SUPPORT_LEVEL),)
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+# comment it out for now since we need use some hidden APIs
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := \
+ $(call all-java-files-under, src) \
+ $(call all-renderscript-files-under, src)
+
+LOCAL_PACKAGE_NAME := SmartCamera
+LOCAL_JNI_SHARED_LIBRARIES := libsmartcamera_jni
+
+include $(BUILD_PACKAGE)
+
+# Include packages in subdirectories
+include $(call all-makefiles-under,$(LOCAL_PATH))
+
+endif
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/AndroidManifest.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/AndroidManifest.xml
new file mode 100644
index 0000000000000..06818682192f2
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/AndroidManifest.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/0002_000390.jpg b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/0002_000390.jpg
new file mode 100644
index 0000000000000..9b4bce470a4a8
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/0002_000390.jpg differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLeyesclosed_100.emd b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLeyesclosed_100.emd
new file mode 100644
index 0000000000000..8c3d81189359a
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLeyesclosed_100.emd differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLjoy_100.emd b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLjoy_100.emd
new file mode 100644
index 0000000000000..4ae3fbde6746b
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLjoy_100.emd differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/ic_launcher-web.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/ic_launcher-web.png
new file mode 100644
index 0000000000000..f1422165a220f
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/ic_launcher-web.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Android.mk b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Android.mk
new file mode 100644
index 0000000000000..616a11b366162
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Android.mk
@@ -0,0 +1,49 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+FILTERFW_NATIVE_PATH := $(call my-dir)
+
+
+#
+# Build module libfilterframework
+#
+LOCAL_PATH := $(FILTERFW_NATIVE_PATH)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := 14
+
+LOCAL_MODULE := libsmartcamera_jni
+
+LOCAL_SRC_FILES := contrast.cpp \
+ brightness.cpp \
+ exposure.cpp \
+ colorspace.cpp \
+ histogram.cpp \
+ frametovalues.cpp \
+ pixelutils.cpp \
+ sobeloperator.cpp \
+ stats_scorer.cpp
+
+LOCAL_STATIC_LIBRARIES += \
+ libcutils
+
+LOCAL_C_INCLUDES += \
+ system/core/include \
+
+LOCAL_NDK_STL_VARIANT := stlport_static
+
+include $(BUILD_SHARED_LIBRARY)
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Application.mk b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Application.mk
new file mode 100644
index 0000000000000..2b93b3c9116e6
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Application.mk
@@ -0,0 +1,16 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+APP_STL := stlport_static
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.cpp
new file mode 100644
index 0000000000000..998fd4cd0f374
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract brightness from image (handed down as ByteBuffer).
+
+#include "brightness.h"
+
+#include
+#include
+#include
+#include
+#include
+
+jfloat
+Java_androidx_media_filterfw_samples_simplecamera_AvgBrightnessFilter_brightnessOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer) {
+
+ if (imageBuffer == 0) {
+ return 0.0f;
+ }
+ float pixelTotals[] = { 0.0f, 0.0f, 0.0f };
+ const int numPixels = width * height;
+ unsigned char* srcPtr = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ for (int i = 0; i < numPixels; i++) {
+ pixelTotals[0] += *(srcPtr + 4 * i);
+ pixelTotals[1] += *(srcPtr + 4 * i + 1);
+ pixelTotals[2] += *(srcPtr + 4 * i + 2);
+ }
+ float avgPixels[] = { 0.0f, 0.0f, 0.0f };
+
+ avgPixels[0] = pixelTotals[0] / numPixels;
+ avgPixels[1] = pixelTotals[1] / numPixels;
+ avgPixels[2] = pixelTotals[2] / numPixels;
+ float returnValue = sqrt(0.241f * avgPixels[0] * avgPixels[0] +
+ 0.691f * avgPixels[1] * avgPixels[1] +
+ 0.068f * avgPixels[2] * avgPixels[2]);
+
+ return returnValue / 255;
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.h
new file mode 100644
index 0000000000000..c09e3b535c9ed
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.h
@@ -0,0 +1,36 @@
+/* Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract brightness from image (handed down as ByteBuffer).
+
+#ifndef ANDROID_FILTERFW_JNI_BRIGHTNESS_H
+#define ANDROID_FILTERFW_JNI_BRIGHTNESS_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ JNIEXPORT jfloat JNICALL
+ Java_androidx_media_filterfw_samples_simplecamera_AvgBrightnessFilter_brightnessOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROID_FILTERFW_JNI_BRIGHTNESS_H
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.cpp
new file mode 100644
index 0000000000000..63e2ebf2f3e7e
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.cpp
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "colorspace.h"
+
+#include
+#include
+
+typedef uint8_t uint8;
+typedef uint32_t uint32;
+typedef int32_t int32;
+
+// RGBA helper struct allows access as int and individual channels
+// WARNING: int value depends on endianness and should not be used to analyze individual channels.
+union Rgba {
+ uint32 color;
+ uint8 channel[4];
+};
+
+// Channel index constants
+static const uint8 kRed = 0;
+static const uint8 kGreen = 1;
+static const uint8 kBlue = 2;
+static const uint8 kAlpha = 3;
+
+// Clamp to range 0-255
+static inline uint32 clamp(int32 x) {
+ return x > 255 ? 255 : (x < 0 ? 0 : x);
+}
+
+// Convert YUV to RGBA
+// This uses the ITU-R BT.601 coefficients.
+static inline Rgba convertYuvToRgba(int32 y, int32 u, int32 v) {
+ Rgba color;
+ color.channel[kRed] = clamp(y + static_cast(1.402 * v));
+ color.channel[kGreen] = clamp(y - static_cast(0.344 * u + 0.714 * v));
+ color.channel[kBlue] = clamp(y + static_cast(1.772 * u));
+ color.channel[kAlpha] = 0xFF;
+ return color;
+}
+
+// Colorspace conversion functions /////////////////////////////////////////////////////////////////
+void JNI_COLORSPACE_METHOD(nativeYuv420pToRgba8888)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height) {
+ uint8* const pInput = static_cast(env->GetDirectBufferAddress(input));
+ Rgba* const pOutput = static_cast(env->GetDirectBufferAddress(output));
+
+ const int size = width * height;
+
+ uint8* pInY = pInput;
+ uint8* pInU = pInput + size;
+ uint8* pInV = pInput + size + size / 4;
+ Rgba* pOutColor = pOutput;
+
+ const int u_offset = size;
+ const int v_offset = u_offset + size / 4;
+
+ for (int y = 0; y < height; y += 2) {
+ for (int x = 0; x < width; x += 2) {
+ int u, v, y1, y2, y3, y4;
+
+ y1 = pInY[0];
+ y2 = pInY[1];
+ y3 = pInY[width];
+ y4 = pInY[width + 1];
+
+ u = *pInU - 128;
+ v = *pInV - 128;
+
+ pOutColor[0] = convertYuvToRgba(y1, u, v);
+ pOutColor[1] = convertYuvToRgba(y2, u, v);
+ pOutColor[width] = convertYuvToRgba(y3, u, v);
+ pOutColor[width + 1] = convertYuvToRgba(y4, u, v);
+
+ pInY += 2;
+ pInU++;
+ pInV++;
+ pOutColor += 2;
+ }
+ pInY += width;
+ pOutColor += width;
+ }
+}
+
+void JNI_COLORSPACE_METHOD(nativeArgb8888ToRgba8888)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height) {
+ Rgba* pInput = static_cast(env->GetDirectBufferAddress(input));
+ Rgba* pOutput = static_cast(env->GetDirectBufferAddress(output));
+
+ for (int i = 0; i < width * height; ++i) {
+ Rgba color_in = *pInput++;
+ Rgba& color_out = *pOutput++;
+ color_out.channel[kRed] = color_in.channel[kGreen];
+ color_out.channel[kGreen] = color_in.channel[kBlue];
+ color_out.channel[kBlue] = color_in.channel[kAlpha];
+ color_out.channel[kAlpha] = color_in.channel[kRed];
+ }
+}
+
+void JNI_COLORSPACE_METHOD(nativeRgba8888ToHsva8888)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height) {
+ Rgba* pInput = static_cast(env->GetDirectBufferAddress(input));
+ Rgba* pOutput = static_cast(env->GetDirectBufferAddress(output));
+
+ int r, g, b, a, h, s, v, c_max, c_min;
+ float delta;
+ for (int i = 0; i < width * height; ++i) {
+ Rgba color_in = *pInput++;
+ Rgba& color_out = *pOutput++;
+ r = color_in.channel[kRed];
+ g = color_in.channel[kGreen];
+ b = color_in.channel[kBlue];
+ a = color_in.channel[kAlpha];
+
+ if (r > g) {
+ c_min = (g > b) ? b : g;
+ c_max = (r > b) ? r : b;
+ } else {
+ c_min = (r > b) ? b : r;
+ c_max = (g > b) ? g : b;
+ }
+ delta = c_max -c_min;
+
+ float scaler = 255 * 60 / 360.0f;
+ if (c_max == r) {
+ h = (g > b) ? static_cast(scaler * (g - b) / delta) :
+ static_cast(scaler * ((g - b) / delta + 6));
+ } else if (c_max == g) {
+ h = static_cast(scaler * ((b - r) / delta + 2));
+ } else { // Cmax == b
+ h = static_cast(scaler * ((r - g) / delta + 4));
+ }
+ s = (delta == 0.0f) ? 0 : static_cast(delta / c_max * 255);
+ v = c_max;
+
+ color_out.channel[kRed] = h;
+ color_out.channel[kGreen] = s;
+ color_out.channel[kBlue] = v;
+ color_out.channel[kAlpha] = a;
+ }
+}
+
+void JNI_COLORSPACE_METHOD(nativeRgba8888ToYcbcra8888)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height) {
+ Rgba* pInput = static_cast(env->GetDirectBufferAddress(input));
+ Rgba* pOutput = static_cast(env->GetDirectBufferAddress(output));
+
+ int r, g, b;
+ for (int i = 0; i < width * height; ++i) {
+ Rgba color_in = *pInput++;
+ Rgba& color_out = *pOutput++;
+ r = color_in.channel[kRed];
+ g = color_in.channel[kGreen];
+ b = color_in.channel[kBlue];
+
+ color_out.channel[kRed] =
+ static_cast((65.738 * r + 129.057 * g + 25.064 * b) / 256 + 16);
+ color_out.channel[kGreen] =
+ static_cast((-37.945 * r - 74.494 * g + 112.439 * b) / 256 + 128);
+ color_out.channel[kBlue] =
+ static_cast((112.439 * r - 94.154 * g - 18.285 * b) / 256 + 128);
+ color_out.channel[kAlpha] = color_in.channel[kAlpha];
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.h
new file mode 100644
index 0000000000000..c33274950e3d7
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_FILTERFW_JNI_COLORSPACE_H
+#define ANDROID_FILTERFW_JNI_COLORSPACE_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define JNI_COLORSPACE_METHOD(METHOD_NAME) \
+ Java_androidx_media_filterfw_ColorSpace_ ## METHOD_NAME
+
+JNIEXPORT void JNICALL
+JNI_COLORSPACE_METHOD(nativeYuv420pToRgba8888)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height);
+
+JNIEXPORT void JNICALL
+JNI_COLORSPACE_METHOD(nativeArgb8888ToRgba8888)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height);
+
+JNIEXPORT void JNICALL
+JNI_COLORSPACE_METHOD(nativeRgba8888ToHsva8888)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height);
+
+JNIEXPORT void JNICALL
+JNI_COLORSPACE_METHOD(nativeRgba8888ToYcbcra8888)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROID_FILTERFW_JNI_COLORSPACE_H
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.cpp
new file mode 100644
index 0000000000000..222f738a3a1dd
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract contrast ratio from image (handed down as ByteBuffer).
+
+#include "contrast.h"
+
+#include
+#include
+#include
+#include
+#include
+
+jfloat
+Java_androidx_media_filterfw_samples_simplecamera_ContrastRatioFilter_contrastOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer) {
+
+ if (imageBuffer == 0) {
+ return 0.0f;
+ }
+ float total = 0;
+ const int numPixels = width * height;
+ unsigned char* srcPtr = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ float* lumArray = new float[numPixels];
+ for (int i = 0; i < numPixels; i++) {
+ lumArray[i] = (0.2126f * *(srcPtr + 4 * i) + 0.7152f *
+ *(srcPtr + 4 * i + 1) + 0.0722f * *(srcPtr + 4 * i + 2)) / 255;
+ total += lumArray[i];
+ }
+ const float avg = total / numPixels;
+ float sum = 0;
+
+ for (int i = 0; i < numPixels; i++) {
+ sum += (lumArray[i] - avg) * (lumArray[i] - avg);
+ }
+ delete[] lumArray;
+ return ((float) sqrt(sum / numPixels));
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.h
new file mode 100644
index 0000000000000..ddcd3d4c5cc15
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.h
@@ -0,0 +1,36 @@
+/* Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract contrast from image (handed down as ByteBuffer).
+
+#ifndef ANDROID_FILTERFW_JNI_CONTRAST_H
+#define ANDROID_FILTERFW_JNI_CONTRAST_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ JNIEXPORT jfloat JNICALL
+ Java_androidx_media_filterfw_samples_simplecamera_ContrastRatioFilter_contrastOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROID_FILTERFW_JNI_CONTRAST_H
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.cpp
new file mode 100644
index 0000000000000..b2853f7b0037a
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.cpp
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract exposure from image (handed down as ByteBuffer).
+
+#include "exposure.h"
+
+#include
+#include
+#include
+#include
+#include
+
+
+jfloat
+Java_androidx_media_filterfw_samples_simplecamera_ExposureFilter_overExposureOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer) {
+ if (imageBuffer == 0) {
+ return 0.0f;
+ }
+ const int numPixels = width * height;
+ unsigned char* srcPtr = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ int output = 0;
+ float tempLuminance = 0.0f;
+
+ for (int i = 0; i < numPixels; i++) {
+ tempLuminance = (0.2126f * *(srcPtr + 4 * i) +
+ 0.7152f * *(srcPtr + 4 * i + 1) +
+ 0.0722f * *(srcPtr + 4 * i + 2));
+ if (tempLuminance + 5 >= 255) {
+ output++;
+ }
+ }
+ return (static_cast(output)) / numPixels;
+}
+
+jfloat
+Java_androidx_media_filterfw_samples_simplecamera_ExposureFilter_underExposureOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer) {
+ if (imageBuffer == 0) {
+ return 0.0f;
+ }
+ const int numPixels = width * height;
+ unsigned char* srcPtr = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ int output = 0;
+ float tempLuminance = 0.0f;
+
+ for (int i = 0; i < numPixels; i++) {
+ tempLuminance = (0.2126f * *(srcPtr + 4 * i) +
+ 0.7152f * *(srcPtr + 4 * i + 1) +
+ 0.0722f * *(srcPtr + 4 * i + 2));
+ if (tempLuminance - 5 <= 0) {
+ output++;
+ }
+ }
+ return (static_cast(output)) / numPixels;
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.h
new file mode 100644
index 0000000000000..bc6e3b127fbcc
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.h
@@ -0,0 +1,39 @@
+/* Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract exposure from image (handed down as ByteBuffer).
+
+#ifndef ANDROID_FILTERFW_JNI_EXPOSURE_H
+#define ANDROID_FILTERFW_JNI_EXPOSURE_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ JNIEXPORT jfloat JNICALL
+ Java_androidx_media_filterfw_samples_simplecamera_ExposureFilter_underExposureOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer);
+
+ JNIEXPORT jfloat JNICALL
+ Java_androidx_media_filterfw_samples_simplecamera_ExposureFilter_overExposureOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer);
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROID_FILTERFW_JNI_EXPOSURE_H
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.cpp
new file mode 100644
index 0000000000000..2e3a0ecb431db
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.cpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract histogram from image (handed down as ByteBuffer).
+
+#include "frametovalues.h"
+
+#include
+#include
+#include
+#include
+
+#include "imgprocutil.h"
+
+jboolean Java_androidx_media_filterpacks_image_ToGrayValuesFilter_toGrayValues(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject grayBuffer )
+{
+ unsigned char* pixelPtr = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ unsigned char* grayPtr = static_cast(env->GetDirectBufferAddress(grayBuffer));
+
+ if (pixelPtr == 0 || grayPtr == 0) {
+ return JNI_FALSE;
+ }
+
+ int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4;
+
+ // TODO: the current implementation is focused on the correctness not performance.
+ // If performance becomes an issue, it is better to increment pixelPtr directly.
+ int disp = 0;
+ for(int idx = 0; idx < numPixels; idx++, disp+=4) {
+ int R = *(pixelPtr + disp);
+ int G = *(pixelPtr + disp + 1);
+ int B = *(pixelPtr + disp + 2);
+ int gray = getIntensityFast(R, G, B);
+ *(grayPtr+idx) = static_cast(gray);
+ }
+
+ return JNI_TRUE;
+}
+
+jboolean Java_androidx_media_filterpacks_image_ToRgbValuesFilter_toRgbValues(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject rgbBuffer )
+{
+ unsigned char* pixelPtr = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ unsigned char* rgbPtr = static_cast(env->GetDirectBufferAddress(rgbBuffer));
+
+ if (pixelPtr == 0 || rgbPtr == 0) {
+ return JNI_FALSE;
+ }
+
+ int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4;
+
+ // TODO: this code could be revised to improve the performance as the TODO above.
+ int pixelDisp = 0;
+ int rgbDisp = 0;
+ for(int idx = 0; idx < numPixels; idx++, pixelDisp += 4, rgbDisp += 3) {
+ for (int c = 0; c < 3; ++c) {
+ *(rgbPtr + rgbDisp + c) = *(pixelPtr + pixelDisp + c);
+ }
+ }
+ return JNI_TRUE;
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.h
new file mode 100644
index 0000000000000..4abb8487ff632
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native functions to pack a RGBA frame into either a one channel grayscale buffer
+// or a three channel RGB buffer.
+
+#ifndef ANDROID_FILTERFW_JNI_TOGRAYVALUES_H
+#define ANDROID_FILTERFW_JNI_TOGRAYVALUES_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT jboolean JNICALL
+Java_androidx_media_filterpacks_image_ToGrayValuesFilter_toGrayValues(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject grayBuffer );
+
+JNIEXPORT jboolean JNICALL
+Java_androidx_media_filterpacks_image_ToRgbValuesFilter_toRgbValues(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject rgbBuffer );
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROID_FILTERFW_JNI_TOGRAYVALUES_H
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.cpp
new file mode 100644
index 0000000000000..ba060d43e1547
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.cpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract histogram from image (handed down as ByteBuffer).
+
+#include "histogram.h"
+
+#include
+#include
+#include
+#include
+
+#include "imgprocutil.h"
+
+inline void addPixelToHistogram(unsigned char*& pImg, int* pHist, int numBins) {
+ int R = *(pImg++);
+ int G = *(pImg++);
+ int B = *(pImg++);
+ ++pImg;
+ int i = getIntensityFast(R, G, B);
+ int bin = clamp(0, static_cast(static_cast(i * numBins) / 255.0f), numBins - 1);
+ ++pHist[bin];
+}
+
+void Java_androidx_media_filterpacks_histogram_GrayHistogramFilter_extractHistogram(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject maskBuffer, jobject histogramBuffer )
+{
+ unsigned char* pImg = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ int* pHist = static_cast(env->GetDirectBufferAddress(histogramBuffer));
+ int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4; // 4 bytes per pixel
+ int numBins = env->GetDirectBufferCapacity(histogramBuffer);
+
+ unsigned char* pMask = NULL;
+ if(maskBuffer != NULL) {
+ pMask = static_cast(env->GetDirectBufferAddress(maskBuffer));
+ }
+
+ for(int i = 0; i < numBins; ++i) pHist[i] = 0;
+
+ if(pMask == NULL) {
+ for( ; numPixels > 0; --numPixels) {
+ addPixelToHistogram(pImg, pHist, numBins);
+ }
+ } else {
+ for( ; numPixels > 0; --numPixels) {
+ if(*pMask == 0){
+ pMask += 4;
+ pImg += 4; // Note that otherwise addPixelToHistogram advances pImg by 4
+ continue;
+ }
+ pMask += 4;
+ addPixelToHistogram(pImg, pHist, numBins);
+ }
+ }
+}
+
+void Java_androidx_media_filterpacks_histogram_ChromaHistogramFilter_extractChromaHistogram(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject histogramBuffer, jint hBins, jint sBins)
+{
+ unsigned char* pixelIn = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ float* histOut = static_cast(env->GetDirectBufferAddress(histogramBuffer));
+ int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4; // 4 bytes per pixel
+
+ for (int i = 0; i < hBins * sBins; ++i) histOut[i] = 0.0f;
+
+ int h, s, v;
+ float hScaler = hBins / 256.0f;
+ float sScaler = sBins / 256.0f;
+ for( ; numPixels > 0; --numPixels) {
+ h = *(pixelIn++);
+ s = *(pixelIn++);
+ v = *(pixelIn++);
+ pixelIn++;
+
+ int index = static_cast(s * sScaler) * hBins + static_cast(h * hScaler);
+ histOut[index] += 1.0f;
+ }
+}
+
+void Java_androidx_media_filterpacks_histogram_NewChromaHistogramFilter_extractChromaHistogram(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject histogramBuffer,
+ jint hueBins, jint saturationBins, jint valueBins,
+ jint saturationThreshold, jint valueThreshold) {
+ unsigned char* pixelIn = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ float* histOut = static_cast(env->GetDirectBufferAddress(histogramBuffer));
+ int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4; // 4 bytes per pixel
+
+ // TODO: add check on the size of histOut
+ for (int i = 0; i < (hueBins * saturationBins + valueBins); ++i) {
+ histOut[i] = 0.0f;
+ }
+
+ for( ; numPixels > 0; --numPixels) {
+ int h = *(pixelIn++);
+ int s = *(pixelIn++);
+ int v = *(pixelIn++);
+
+ pixelIn++;
+ // If a pixel that is either too dark (less than valueThreshold) or colorless
+ // (less than saturationThreshold), if will be put in a 1-D value histogram instead.
+
+ int index;
+ if (s > saturationThreshold && v > valueThreshold) {
+ int sIndex = s * saturationBins / 256;
+
+ // Shifting hue index by 0.5 such that peaks of red, yellow, green, cyan, blue, pink
+ // will be at the center of some bins.
+ int hIndex = ((h * hueBins + 128) / 256) % hueBins;
+ index = sIndex * hueBins + hIndex;
+ } else {
+ index = hueBins * saturationBins + (v * valueBins / 256);
+ }
+ histOut[index] += 1.0f;
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.h
new file mode 100644
index 0000000000000..b5e88aadfa06d
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract histogram from image (handed down as ByteBuffer).
+
+#ifndef ANDROID_FILTERFW_JNI_HISTOGRAM_H
+#define ANDROID_FILTERFW_JNI_HISTOGRAM_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT void JNICALL
+Java_androidx_media_filterpacks_histogram_GrayHistogramFilter_extractHistogram(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject maskBuffer, jobject histogramBuffer );
+
+JNIEXPORT void JNICALL
+Java_androidx_media_filterpacks_histogram_ChromaHistogramFilter_extractChromaHistogram(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject histogramBuffer, jint hBins, jint sBins);
+
+JNIEXPORT void JNICALL
+Java_androidx_media_filterpacks_histogram_NewChromaHistogramFilter_extractChromaHistogram(
+ JNIEnv* env, jclass clazz, jobject imageBuffer, jobject histogramBuffer,
+ jint hueBins, jint saturationBins, jint valueBins,
+ jint saturationThreshold, jint valueThreshold);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROID_FILTERFW_JNI_HISTOGRAM_H
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/imgprocutil.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/imgprocutil.h
new file mode 100644
index 0000000000000..aef67a50500b3
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/imgprocutil.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Some native low-level image processing functions.
+
+
+#ifndef ANDROID_FILTERFW_JNI_IMGPROCUTIL_H
+#define ANDROID_FILTERFW_JNI_IMGPROCUTIL_H
+
+inline int getIntensityFast(int R, int G, int B) {
+ return (R + R + R + B + G + G + G + G) >> 3; // see http://stackoverflow.com/a/596241
+}
+
+inline int clamp(int min, int val, int max) {
+ return val < min ? min : (val > max ? max : val);
+ // Note that for performance reasons, this function does *not* check if min < max!
+}
+
+#endif // ANDROID_FILTERFW_JNI_IMGPROCUTIL_H
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.cpp
new file mode 100644
index 0000000000000..596c7c0f5563a
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "pixelutils.h"
+
+#include
+
+typedef uint32_t uint32;
+
+void JNI_PIXELUTILS_METHOD(nativeCopyPixels)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height, jint offset,
+ jint pixStride, jint rowStride) {
+ uint32* pInPix = static_cast(env->GetDirectBufferAddress(input));
+ uint32* pOutput = static_cast(env->GetDirectBufferAddress(output));
+ uint32* pOutRow = pOutput + offset;
+ for (int y = 0; y < height; ++y) {
+ uint32* pOutPix = pOutRow;
+ for (int x = 0; x < width; ++x) {
+ *pOutPix = *(pInPix++);
+ pOutPix += pixStride;
+ }
+ pOutRow += rowStride;
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.h
new file mode 100644
index 0000000000000..be690099fef48
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ANDROID_FILTERFW_JNI_PIXELUTILS_H
+#define ANDROID_FILTERFW_JNI_PIXELUTILS_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define JNI_PIXELUTILS_METHOD(METHOD_NAME) \
+ Java_androidx_media_filterfw_PixelUtils_ ## METHOD_NAME
+
+JNIEXPORT void JNICALL
+JNI_PIXELUTILS_METHOD(nativeCopyPixels)(
+ JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height, jint offset,
+ jint pixStride, jint rowStride);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROID_FILTERFW_JNI_PIXELUTILS_H
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.cpp
new file mode 100644
index 0000000000000..dc5c305ea23f5
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.cpp
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract histogram from image (handed down as ByteBuffer).
+
+#include "sobeloperator.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "imgprocutil.h"
+
+/*
+ * Perform 1d convolution on 3 channel image either horizontally or vertically.
+ * Parameters:
+ * inputHead: pointer to input image
+ * length: the length of image in the chosen axis.
+ * fragments: number of lines of the image in the chosen axis.
+ * step: the 1d pixel distance between adjacent pixels in the chosen axis.
+ * shift: the 1d pixel distance between adjacent lines in the chosen axis.
+ * filter: pointer to 1d filter
+ * halfSize: the length of filter is supposed to be (2 * halfSize + 1)
+ * outputHead: pointer to output image
+ */
+
+void computeGradient(unsigned char* dataPtr, int width, int height, short* gxPtr, short* gyPtr) {
+ for (int i = 0; i < height; i++) {
+ for (int j = 0; j < width; j++) {
+ const int left = (j > 0)? -4 : 0;
+ const int right = (j < width - 1) ? 4 : 0;
+ const int curr = (i * width + j) * 4;
+ const int above = (i > 0) ? curr - 4 * width : curr;
+ const int below = (i < height - 1) ? curr + 4 * width : curr;
+ const int offset = (i * width + j) * 3;
+ for (int c = 0; c < 3; c++) {
+ *(gxPtr + offset + c) =
+ (*(dataPtr + curr + c + right) - *(dataPtr + curr + c + left)) * 2 +
+ *(dataPtr + above + c + right) - *(dataPtr + above + c + left) +
+ *(dataPtr + below + c + right) - *(dataPtr + below + c + left);
+ *(gyPtr + offset + c) =
+ (*(dataPtr + c + below) - *(dataPtr + c + above)) * 2 +
+ *(dataPtr + left + c + below) - *(dataPtr + left + c + above) +
+ *(dataPtr + right + c + below) - *(dataPtr + right + c + above);
+ }
+ }
+ }
+}
+
+jboolean Java_androidx_media_filterpacks_image_SobelFilter_sobelOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer,
+ jobject magBuffer, jobject dirBuffer) {
+
+ if (imageBuffer == 0) {
+ return JNI_FALSE;
+ }
+ unsigned char* srcPtr = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ unsigned char* magPtr = (magBuffer == 0) ?
+ 0 : static_cast(env->GetDirectBufferAddress(magBuffer));
+ unsigned char* dirPtr = (dirBuffer == 0) ?
+ 0 : static_cast(env->GetDirectBufferAddress(dirBuffer));
+
+ int numPixels = width * height;
+ // TODO: avoid creating and deleting these buffers within this native function.
+ short* gxPtr = new short[3 * numPixels];
+ short* gyPtr = new short[3 * numPixels];
+ computeGradient(srcPtr, width, height, gxPtr, gyPtr);
+
+ unsigned char* mag = magPtr;
+ unsigned char* dir = dirPtr;
+ for (int i = 0; i < numPixels; ++i) {
+ for (int c = 0; c < 3; c++) {
+ int gx = static_cast(*(gxPtr + 3 * i + c) / 8 + 127.5);
+ int gy = static_cast(*(gyPtr + 3 * i + c) / 8 + 127.5);
+
+ // emulate arithmetic in GPU.
+ gx = 2 * gx - 255;
+ gy = 2 * gy - 255;
+ if (magPtr != 0) {
+ double value = sqrt(gx * gx + gy * gy);
+ *(magPtr + 4 * i + c) = static_cast(value);
+ }
+ if (dirPtr != 0) {
+ *(dirPtr + 4 * i + c) = static_cast(
+ (atan(static_cast(gy)/static_cast(gx)) + 3.14) / 6.28);
+ }
+ }
+ //setting alpha change to 1.0 (255)
+ if (magPtr != 0) {
+ *(magPtr + 4 * i + 3) = 255;
+ }
+ if (dirPtr != 0) {
+ *(dirPtr + 4 * i + 3) = 255;
+ }
+ }
+
+ delete[] gxPtr;
+ delete[] gyPtr;
+
+ return JNI_TRUE;
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.h
new file mode 100644
index 0000000000000..c7639d22ecccf
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Native function to extract histogram from image (handed down as ByteBuffer).
+
+#ifndef ANDROID_FILTERFW_JNI_SOBELOPERATOR_H
+#define ANDROID_FILTERFW_JNI_SOBELOPERATOR_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT jboolean JNICALL
+Java_androidx_media_filterpacks_image_SobelFilter_sobelOperator(
+ JNIEnv* env, jclass clazz, jint width, jint height,
+ jobject imageBuffer, jobject magBuffer, jobject dirBuffer);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // ANDROID_FILTERFW_JNI_SOBELOPERATOR_H
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.cpp
new file mode 100644
index 0000000000000..f2826756c615e
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.cpp
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Stats (mean and stdev) scoring in the native.
+
+#include "stats_scorer.h"
+
+#include
+#include
+
+void Java_androidx_media_filterpacks_numeric_StatsFilter_score(
+ JNIEnv* env, jobject thiz, jobject imageBuffer, jfloatArray statsArray)
+{
+ unsigned char* pImg = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ int numPixels = env->GetDirectBufferCapacity(imageBuffer); // 1 byte per pixel
+ float sum = 0.0;
+ float sumSquares = 0.0;
+
+ for (int i = 0; i < numPixels; ++i) {
+ float val = static_cast(pImg[i]);
+ sum += val;
+ sumSquares += val * val;
+ }
+ jfloat result[2];
+ result[0] = sum / numPixels; // mean
+ result[1] = sqrt((sumSquares - numPixels * result[0] * result[0]) / (numPixels - 1)); // stdev.
+ env->SetFloatArrayRegion(statsArray, 0, 2, result);
+}
+
+void Java_androidx_media_filterpacks_numeric_StatsFilter_regionscore(
+ JNIEnv* env, jobject thiz, jobject imageBuffer, jint width, jint height,
+ jfloat left, jfloat top, jfloat right, jfloat bottom, jfloatArray statsArray)
+{
+ unsigned char* pImg = static_cast(env->GetDirectBufferAddress(imageBuffer));
+ int xStart = static_cast(width * left);
+ int xEnd = static_cast(width * right);
+ int yStart = static_cast(height * top);
+ int yEnd = static_cast(height * bottom);
+ int numPixels = (xEnd - xStart) * (yEnd - yStart);
+ float sum = 0.0;
+ float sumSquares = 0.0;
+
+ for (int y = yStart; y < yEnd; y++) {
+ int disp = width * y;
+ for (int x = xStart; x < xEnd; ++x) {
+ float val = static_cast(*(pImg + disp + x));
+ sum += val;
+ sumSquares += val * val;
+ }
+ }
+ jfloat result[2];
+ result[0] = sum / numPixels; // mean
+ result[1] = (numPixels == 1) ?
+ 0 : sqrt((sumSquares - numPixels * result[0] * result[0]) / (numPixels - 1)); // stdev.
+ env->SetFloatArrayRegion(statsArray, 0, 2, result);
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.h
new file mode 100644
index 0000000000000..a951ec9135db1
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Stats (mean and stdev) scoring in the native.
+
+#ifndef ANDROID_FILTERFW_JNI_STATS_SCORER_H
+#define ANDROID_FILTERFW_JNI_STATS_SCORER_H
+
+#include
+
+#define JNI_FES_FUNCTION(name) Java_androidx_media_filterpacks_numeric_StatsFilter_ ## name
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+JNIEXPORT void JNICALL
+JNI_FES_FUNCTION(score)(
+ JNIEnv* env, jobject thiz, jobject imageBuffer, jfloatArray statsArray);
+
+JNIEXPORT void JNICALL
+JNI_FES_FUNCTION(regionscore)(
+ JNIEnv* env, jobject thiz, jobject imageBuffer, jint width, jint height,
+ jfloat lefp, jfloat top, jfloat right, jfloat bottom, jfloatArray statsArray);
+
+#ifdef __cplusplus
+}
+#endif
+
+
+#endif // ANDROID_FILTERFW_JNI_STATS_SCORER_H
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/proguard-project.txt b/tests/Camera2Tests/SmartCamera/SimpleCamera/proguard-project.txt
new file mode 100644
index 0000000000000..f2fe1559a2178
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/project.properties b/tests/Camera2Tests/SmartCamera/SimpleCamera/project.properties
new file mode 100644
index 0000000000000..10149cb17b5c1
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-16
+android.library.reference.1=../../filterfw
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/black_screen.jpg b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/black_screen.jpg
new file mode 100644
index 0000000000000..702d9fa8da52a
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/black_screen.jpg differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_launcher.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000000000..1c7b44a6fdb46
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_launcher.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_gallery.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_gallery.png
new file mode 100644
index 0000000000000..f61bbd8bae606
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_gallery.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_quill.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_quill.png
new file mode 100644
index 0000000000000..7ea01b7869111
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_quill.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_save.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_save.png
new file mode 100644
index 0000000000000..62d0b9a00b810
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_save.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-ldpi/ic_launcher.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000000000..b42e90393fb69
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-ldpi/ic_launcher.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-mdpi/ic_launcher.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000000000..d4b4d6b462101
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-mdpi/ic_launcher.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/android_figure.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/android_figure.png
new file mode 100644
index 0000000000000..71c6d760f0518
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/android_figure.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/oldframe.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/oldframe.png
new file mode 100644
index 0000000000000..8b7ae63739ea6
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/oldframe.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/polaroid.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/polaroid.png
new file mode 100644
index 0000000000000..5504c5730959a
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/polaroid.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-xhdpi/ic_launcher.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000000..3bb5454e3de91
Binary files /dev/null and b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/imageview.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/imageview.xml
new file mode 100644
index 0000000000000..4e20c3fbcbea0
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/imageview.xml
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/simplecamera.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/simplecamera.xml
new file mode 100644
index 0000000000000..8d8ff51154cf1
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/simplecamera.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/raw/camera_graph.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/raw/camera_graph.xml
new file mode 100644
index 0000000000000..6661fd7c55c83
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/raw/camera_graph.xml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v11/styles.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v11/styles.xml
new file mode 100644
index 0000000000000..d408cbc37ad42
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v11/styles.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v14/styles.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v14/styles.xml
new file mode 100644
index 0000000000000..1c089a788c5a7
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v14/styles.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/strings.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/strings.xml
new file mode 100644
index 0000000000000..5e6b8aba3e524
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/strings.xml
@@ -0,0 +1,38 @@
+
+
+
+
+ Good/Bad Picture
+ FPS
+ Score
+ Go To Gallery
+ Go To Camera
+ Start
+ Images Saved
+ Gallery
+
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/styles.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/styles.xml
new file mode 100644
index 0000000000000..bd5027f137f1a
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/styles.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BackingStore.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BackingStore.java
new file mode 100644
index 0000000000000..216e743938cac
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BackingStore.java
@@ -0,0 +1,929 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.Type;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Vector;
+
+final class BackingStore {
+
+ /** Access mode None: Frame data will not be accessed at all. */
+ static final int ACCESS_NONE = 0x00;
+ /** Access mode Bytes: Frame data will be accessed as a ByteBuffer. */
+ static final int ACCESS_BYTES = 0x01;
+ /** Access mode Texture: Frame data will be accessed as a TextureSource. */
+ static final int ACCESS_TEXTURE = 0x02;
+ /** Access mode RenderTarget: Frame data will be accessed as a RenderTarget. */
+ static final int ACCESS_RENDERTARGET = 0x04;
+ /** Access mode Object: Frame data will be accessed as a generic Object. */
+ static final int ACCESS_OBJECT = 0x08;
+ /** Access mode Bitmap: Frame data will be accessed as a Bitmap. */
+ static final int ACCESS_BITMAP = 0x10;
+ /** Access mode Allocation: Frame data will be accessed as a RenderScript Allocation. */
+ static final int ACCESS_ALLOCATION = 0x20;
+
+ private static final int BACKING_BYTEBUFFER = 1;
+ private static final int BACKING_TEXTURE = 2;
+ private static final int BACKING_OBJECT = 3;
+ private static final int BACKING_BITMAP = 4;
+ private static final int BACKING_ALLOCATION = 5;
+
+ private final FrameType mType;
+ private int[] mDimensions;
+ private long mTimestamp = Frame.TIMESTAMP_NOT_SET;
+
+ private final FrameManager mFrameManager;
+
+ private Vector mBackings = new Vector();
+
+ private boolean mWriteLocked = false;
+ private int mReadLocks = 0;
+
+ private int mRefCount = 1;
+
+ /** The most up-to-date data backing */
+ private Backing mCurrentBacking = null;
+
+ /** The currently locked backing */
+ private Backing mLockedBacking = null;
+
+ // Public Methods //////////////////////////////////////////////////////////////////////////////
+ public BackingStore(FrameType type, int[] dimensions, FrameManager frameManager) {
+ mType = type;
+ mDimensions = dimensions != null ? Arrays.copyOf(dimensions, dimensions.length) : null;
+ mFrameManager = frameManager;
+ }
+
+ public FrameType getFrameType() {
+ return mType;
+ }
+
+ public Object lockData(int mode, int accessFormat) {
+ return lockBacking(mode, accessFormat).lock(accessFormat);
+ }
+
+ public Backing lockBacking(int mode, int access) {
+ Backing backing = fetchBacking(mode, access);
+ if (backing == null) {
+ throw new RuntimeException("Could not fetch frame data!");
+ }
+ lock(backing, mode);
+ return backing;
+ }
+
+ public boolean unlock() {
+ if (mWriteLocked) {
+ mWriteLocked = false;
+ } else if (mReadLocks > 0) {
+ --mReadLocks;
+ } else {
+ return false;
+ }
+ mLockedBacking.unlock();
+ mLockedBacking = null;
+ return true;
+ }
+
+ public BackingStore retain() {
+ if (mRefCount >= 10) {
+ Log.w("BackingStore", "High ref-count of " + mRefCount + " on " + this + "!");
+ }
+ if (mRefCount <= 0) {
+ throw new RuntimeException("RETAINING RELEASED");
+ }
+ ++mRefCount;
+ return this;
+ }
+
+ public BackingStore release() {
+ if (mRefCount <= 0) {
+ throw new RuntimeException("DOUBLE-RELEASE");
+ }
+ --mRefCount;
+ if (mRefCount == 0) {
+ releaseBackings();
+ return null;
+ }
+ return this;
+ }
+
+ /**
+ * Resizes the backing store. This invalidates all data in the store.
+ */
+ public void resize(int[] newDimensions) {
+ Vector resized = new Vector();
+ for (Backing backing : mBackings) {
+ if (backing.resize(newDimensions)) {
+ resized.add(backing);
+ } else {
+ releaseBacking(backing);
+ }
+ }
+ mBackings = resized;
+ mDimensions = newDimensions;
+ }
+
+ public int[] getDimensions() {
+ return mDimensions;
+ }
+
+ public int getElementCount() {
+ int result = 1;
+ if (mDimensions != null) {
+ for (int dim : mDimensions) {
+ result *= dim;
+ }
+ }
+ return result;
+ }
+
+ public void importStore(BackingStore store) {
+ // TODO: Better backing selection?
+ if (store.mBackings.size() > 0) {
+ importBacking(store.mBackings.firstElement());
+ }
+ mTimestamp = store.mTimestamp;
+ }
+
+ /**
+ * @return the timestamp
+ */
+ public long getTimestamp() {
+ return mTimestamp;
+ }
+
+ /**
+ * @param timestamp the timestamp to set
+ */
+ public void setTimestamp(long timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ // Internal Methods ////////////////////////////////////////////////////////////////////////////
+ private Backing fetchBacking(int mode, int access) {
+ Backing backing = getBacking(mode, access);
+ if (backing == null) {
+ backing = attachNewBacking(mode, access);
+ }
+ syncBacking(backing);
+ return backing;
+ }
+
+ private void syncBacking(Backing backing) {
+ if (backing != null && backing.isDirty() && mCurrentBacking != null) {
+ backing.syncTo(mCurrentBacking);
+ }
+ }
+
+ private Backing getBacking(int mode, int access) {
+ // [Non-iterator looping]
+ for (int i = 0; i < mBackings.size(); ++i) {
+ final Backing backing = mBackings.get(i);
+
+ int backingAccess =
+ (mode == Frame.MODE_WRITE) ? backing.writeAccess() : backing.readAccess();
+ if ((backingAccess & access) == access) {
+ return backing;
+ }
+ }
+ return null;
+ }
+
+ private Backing attachNewBacking(int mode, int access) {
+ Backing backing = createBacking(mode, access);
+ if (mBackings.size() > 0) {
+ backing.markDirty();
+ }
+ mBackings.add(backing);
+ return backing;
+ }
+
+ private Backing createBacking(int mode, int access) {
+ // TODO: If the read/write access flags indicate, make/fetch a GraphicBuffer backing.
+ Backing backing = null;
+ int elemSize = mType.getElementSize();
+ if (shouldFetchCached(access)) {
+ backing = mFrameManager.fetchBacking(mode, access, mDimensions, elemSize);
+ }
+ if (backing == null) {
+ switch (access) {
+ case ACCESS_BYTES:
+ backing = new ByteBufferBacking();
+ break;
+ case ACCESS_TEXTURE:
+ case ACCESS_RENDERTARGET:
+ backing = new TextureBacking();
+ break;
+ case ACCESS_OBJECT:
+ backing = new ObjectBacking();
+ break;
+ case ACCESS_BITMAP:
+ backing = new BitmapBacking();
+ break;
+ case ACCESS_ALLOCATION:
+ if (!AllocationBacking.isSupported()) {
+ throw new RuntimeException(
+ "Attempted to create an AllocationBacking in context that does " +
+ "not support RenderScript!");
+ }
+ backing = new AllocationBacking(mFrameManager.getContext().getRenderScript());
+ break;
+ }
+ if (backing == null) {
+ throw new RuntimeException(
+ "Could not create backing for access type " + access + "!");
+ }
+ if (backing.requiresGpu() && !mFrameManager.getRunner().isOpenGLSupported()) {
+ throw new RuntimeException(
+ "Cannot create backing that requires GPU in a runner that does not " +
+ "support OpenGL!");
+ }
+ backing.setDimensions(mDimensions);
+ backing.setElementSize(elemSize);
+ backing.setElementId(mType.getElementId());
+ backing.allocate(mType);
+ mFrameManager.onBackingCreated(backing);
+ }
+ return backing;
+ }
+
+ private void importBacking(Backing backing) {
+ // TODO: This actually needs synchronization between the two BackingStore threads for the
+ // general case
+ int access = backing.requiresGpu() ? ACCESS_BYTES : backing.readAccess();
+ Backing newBacking = createBacking(Frame.MODE_READ, access);
+ newBacking.syncTo(backing);
+ mBackings.add(newBacking);
+ mCurrentBacking = newBacking;
+ }
+
+ private void releaseBackings() {
+ // [Non-iterator looping]
+ for (int i = 0; i < mBackings.size(); ++i) {
+ releaseBacking(mBackings.get(i));
+ }
+ mBackings.clear();
+ mCurrentBacking = null;
+ }
+
+ private void releaseBacking(Backing backing) {
+ mFrameManager.onBackingAvailable(backing);
+ }
+
+ private void lock(Backing backingToLock, int mode) {
+ if (mode == Frame.MODE_WRITE) {
+ // Make sure frame is not read-locked
+ if (mReadLocks > 0) {
+ throw new RuntimeException(
+ "Attempting to write-lock the read-locked frame " + this + "!");
+ } else if (mWriteLocked) {
+ throw new RuntimeException(
+ "Attempting to write-lock the write-locked frame " + this + "!");
+ }
+ // Mark all other backings dirty
+ // [Non-iterator looping]
+ for (int i = 0; i < mBackings.size(); ++i) {
+ final Backing backing = mBackings.get(i);
+ if (backing != backingToLock) {
+ backing.markDirty();
+ }
+ }
+ mWriteLocked = true;
+ mCurrentBacking = backingToLock;
+ } else {
+ if (mWriteLocked) {
+ throw new RuntimeException("Attempting to read-lock locked frame " + this + "!");
+ }
+ ++mReadLocks;
+ }
+ mLockedBacking = backingToLock;
+ }
+
+ private static boolean shouldFetchCached(int access) {
+ return access != ACCESS_OBJECT;
+ }
+
+
+ // Backings ////////////////////////////////////////////////////////////////////////////////////
+ static abstract class Backing {
+ protected int[] mDimensions = null;
+ private int mElementSize;
+ private int mElementID;
+ protected boolean mIsDirty = false;
+
+ int cachePriority = 0;
+
+ public abstract void allocate(FrameType frameType);
+
+ public abstract int readAccess();
+
+ public abstract int writeAccess();
+
+ public abstract void syncTo(Backing backing);
+
+ public abstract Object lock(int accessType);
+
+ public abstract int getType();
+
+ public abstract boolean shouldCache();
+
+ public abstract boolean requiresGpu();
+
+ public abstract void destroy();
+
+ public abstract int getSize();
+
+ public void unlock() {
+ // Default implementation does nothing.
+ }
+
+ public void setData(Object data) {
+ throw new RuntimeException("Internal error: Setting data on frame backing " + this
+ + ", which does not support setting data directly!");
+ }
+
+ public void setDimensions(int[] dimensions) {
+ mDimensions = dimensions;
+ }
+
+ public void setElementSize(int elemSize) {
+ mElementSize = elemSize;
+ }
+
+ public void setElementId(int elemId) {
+ mElementID = elemId;
+ }
+
+ public int[] getDimensions() {
+ return mDimensions;
+ }
+
+ public int getElementSize() {
+ return mElementSize;
+ }
+
+ public int getElementId() {
+ return mElementID;
+ }
+
+ public boolean resize(int[] newDimensions) {
+ return false;
+ }
+
+ public void markDirty() {
+ mIsDirty = true;
+ }
+
+ public boolean isDirty() {
+ return mIsDirty;
+ }
+
+ protected void assertImageCompatible(FrameType type) {
+ if (type.getElementId() != FrameType.ELEMENT_RGBA8888) {
+ throw new RuntimeException("Cannot allocate texture with non-RGBA data type!");
+ } else if (mDimensions == null || mDimensions.length != 2) {
+ throw new RuntimeException("Cannot allocate non 2-dimensional texture!");
+ }
+ }
+
+ }
+
+ static class ObjectBacking extends Backing {
+
+ private Object mObject = null;
+
+ @Override
+ public void allocate(FrameType frameType) {
+ mObject = null;
+ }
+
+ @Override
+ public int readAccess() {
+ return ACCESS_OBJECT;
+ }
+
+ @Override
+ public int writeAccess() {
+ return ACCESS_OBJECT;
+ }
+
+ @Override
+ public void syncTo(Backing backing) {
+ switch (backing.getType()) {
+ case BACKING_OBJECT:
+ mObject = backing.lock(ACCESS_OBJECT);
+ backing.unlock();
+ break;
+ case BACKING_BITMAP:
+ mObject = backing.lock(ACCESS_BITMAP);
+ backing.unlock();
+ break;
+ default:
+ mObject = null;
+ }
+ mIsDirty = false;
+ }
+
+ @Override
+ public Object lock(int accessType) {
+ return mObject;
+ }
+
+ @Override
+ public int getType() {
+ return BACKING_OBJECT;
+ }
+
+ @Override
+ public boolean shouldCache() {
+ return false;
+ }
+
+ @Override
+ public boolean requiresGpu() {
+ return false;
+ }
+
+ @Override
+ public void destroy() {
+ mObject = null;
+ }
+
+ @Override
+ public int getSize() {
+ return 0;
+ }
+
+ @Override
+ public void setData(Object data) {
+ mObject = data;
+ }
+
+ }
+
+ static class BitmapBacking extends Backing {
+
+ private Bitmap mBitmap = null;
+
+ @Override
+ public void allocate(FrameType frameType) {
+ assertImageCompatible(frameType);
+ }
+
+ @Override
+ public int readAccess() {
+ return ACCESS_BITMAP;
+ }
+
+ @Override
+ public int writeAccess() {
+ return ACCESS_BITMAP;
+ }
+
+ @Override
+ public void syncTo(Backing backing) {
+ int access = backing.readAccess();
+ if ((access & ACCESS_BITMAP) != 0) {
+ mBitmap = (Bitmap) backing.lock(ACCESS_BITMAP);
+ } else if ((access & ACCESS_BYTES) != 0) {
+ createBitmap();
+ ByteBuffer buffer = (ByteBuffer) backing.lock(ACCESS_BYTES);
+ mBitmap.copyPixelsFromBuffer(buffer);
+ buffer.rewind();
+ } else if ((access & ACCESS_TEXTURE) != 0) {
+ createBitmap();
+ RenderTarget renderTarget = (RenderTarget) backing.lock(ACCESS_RENDERTARGET);
+ mBitmap.copyPixelsFromBuffer(
+ renderTarget.getPixelData(mDimensions[0], mDimensions[1]));
+ } else if ((access & ACCESS_ALLOCATION) != 0 && AllocationBacking.isSupported()) {
+ createBitmap();
+ syncToAllocationBacking(backing);
+ } else {
+ throw new RuntimeException("Cannot sync bytebuffer backing!");
+ }
+ backing.unlock();
+ mIsDirty = false;
+ }
+
+ @TargetApi(11)
+ private void syncToAllocationBacking(Backing backing) {
+ Allocation allocation = (Allocation) backing.lock(ACCESS_ALLOCATION);
+ allocation.copyTo(mBitmap);
+ }
+
+ @Override
+ public Object lock(int accessType) {
+ return mBitmap;
+ }
+
+ @Override
+ public int getType() {
+ return BACKING_BITMAP;
+ }
+
+ @Override
+ public boolean shouldCache() {
+ return false;
+ }
+
+ @Override
+ public boolean requiresGpu() {
+ return false;
+ }
+
+ @Override
+ public void destroy() {
+ // As we share the bitmap with other backings (such as object backings), we must not
+ // recycle it here.
+ mBitmap = null;
+ }
+
+ @Override
+ public int getSize() {
+ return 4 * mDimensions[0] * mDimensions[1];
+ }
+
+ @Override
+ public void setData(Object data) {
+ // We can assume that data will always be a Bitmap instance.
+ mBitmap = (Bitmap) data;
+ }
+
+ private void createBitmap() {
+ mBitmap = Bitmap.createBitmap(mDimensions[0], mDimensions[1], Bitmap.Config.ARGB_8888);
+ }
+ }
+
+ static class TextureBacking extends Backing {
+
+ private RenderTarget mRenderTarget = null;
+ private TextureSource mTexture = null;
+
+ @Override
+ public void allocate(FrameType frameType) {
+ assertImageCompatible(frameType);
+ mTexture = TextureSource.newTexture();
+ }
+
+ @Override
+ public int readAccess() {
+ return ACCESS_TEXTURE;
+ }
+
+ @Override
+ public int writeAccess() {
+ return ACCESS_RENDERTARGET;
+ }
+
+ @Override
+ public void syncTo(Backing backing) {
+ int access = backing.readAccess();
+ if ((access & ACCESS_BYTES) != 0) {
+ ByteBuffer pixels = (ByteBuffer) backing.lock(ACCESS_BYTES);
+ mTexture.allocateWithPixels(pixels, mDimensions[0], mDimensions[1]);
+ } else if ((access & ACCESS_BITMAP) != 0) {
+ Bitmap bitmap = (Bitmap) backing.lock(ACCESS_BITMAP);
+ mTexture.allocateWithBitmapPixels(bitmap);
+ } else if ((access & ACCESS_TEXTURE) != 0) {
+ TextureSource texture = (TextureSource) backing.lock(ACCESS_TEXTURE);
+ int w = mDimensions[0];
+ int h = mDimensions[1];
+ ImageShader.renderTextureToTarget(texture, getRenderTarget(), w, h);
+ } else if ((access & ACCESS_ALLOCATION) != 0 && AllocationBacking.isSupported()) {
+ syncToAllocationBacking(backing);
+ } else {
+ throw new RuntimeException("Cannot sync bytebuffer backing!");
+ }
+ backing.unlock();
+ mIsDirty = false;
+ }
+
+ @TargetApi(11)
+ private void syncToAllocationBacking(Backing backing) {
+ Allocation allocation = (Allocation) backing.lock(ACCESS_ALLOCATION);
+ ByteBuffer pixels = ByteBuffer.allocateDirect(getSize());
+ allocation.copyTo(pixels.array());
+ mTexture.allocateWithPixels(pixels, mDimensions[0], mDimensions[1]);
+ }
+
+ @Override
+ public Object lock(int accessType) {
+ switch (accessType) {
+ case ACCESS_TEXTURE:
+ return getTexture();
+
+ case ACCESS_RENDERTARGET:
+ return getRenderTarget();
+
+ default:
+ throw new RuntimeException("Illegal access to texture!");
+ }
+ }
+
+ @Override
+ public int getType() {
+ return BACKING_TEXTURE;
+ }
+
+ @Override
+ public boolean shouldCache() {
+ return true;
+ }
+
+ @Override
+ public boolean requiresGpu() {
+ return true;
+ }
+
+ @Override
+ public void destroy() {
+ if (mRenderTarget != null) {
+ mRenderTarget.release();
+ }
+ if (mTexture.isAllocated()) {
+ mTexture.release();
+ }
+ }
+
+ @Override
+ public int getSize() {
+ return 4 * mDimensions[0] * mDimensions[1];
+ }
+
+ private TextureSource getTexture() {
+ if (!mTexture.isAllocated()) {
+ mTexture.allocate(mDimensions[0], mDimensions[1]);
+ }
+ return mTexture;
+ }
+
+ private RenderTarget getRenderTarget() {
+ if (mRenderTarget == null) {
+ int w = mDimensions[0];
+ int h = mDimensions[1];
+ mRenderTarget = RenderTarget.currentTarget().forTexture(getTexture(), w, h);
+ }
+ return mRenderTarget;
+ }
+
+ }
+
+ static class ByteBufferBacking extends Backing {
+
+ ByteBuffer mBuffer = null;
+
+ @Override
+ public void allocate(FrameType frameType) {
+ int size = frameType.getElementSize();
+ for (int dim : mDimensions) {
+ size *= dim;
+ }
+ mBuffer = ByteBuffer.allocateDirect(size);
+ }
+
+ @Override
+ public int readAccess() {
+ return ACCESS_BYTES;
+ }
+
+ @Override
+ public int writeAccess() {
+ return ACCESS_BYTES;
+ }
+
+ @Override
+ public boolean requiresGpu() {
+ return false;
+ }
+
+ @Override
+ public void syncTo(Backing backing) {
+ int access = backing.readAccess();
+ if ((access & ACCESS_TEXTURE) != 0) {
+ RenderTarget target = (RenderTarget) backing.lock(ACCESS_RENDERTARGET);
+ GLToolbox.readTarget(target, mBuffer, mDimensions[0], mDimensions[1]);
+ } else if ((access & ACCESS_BITMAP) != 0) {
+ Bitmap bitmap = (Bitmap) backing.lock(ACCESS_BITMAP);
+ bitmap.copyPixelsToBuffer(mBuffer);
+ mBuffer.rewind();
+ } else if ((access & ACCESS_BYTES) != 0) {
+ ByteBuffer otherBuffer = (ByteBuffer) backing.lock(ACCESS_BYTES);
+ mBuffer.put(otherBuffer);
+ otherBuffer.rewind();
+ } else if ((access & ACCESS_ALLOCATION) != 0 && AllocationBacking.isSupported()) {
+ syncToAllocationBacking(backing);
+ } else {
+ throw new RuntimeException("Cannot sync bytebuffer backing!");
+ }
+ backing.unlock();
+ mBuffer.rewind();
+ mIsDirty = false;
+ }
+
+ @TargetApi(11)
+ private void syncToAllocationBacking(Backing backing) {
+ Allocation allocation = (Allocation) backing.lock(ACCESS_ALLOCATION);
+ if (getElementId() == FrameType.ELEMENT_RGBA8888) {
+ byte[] bytes = mBuffer.array();
+ allocation.copyTo(bytes);
+ } else if (getElementId() == FrameType.ELEMENT_FLOAT32) {
+ float[] floats = new float[getSize() / 4];
+ allocation.copyTo(floats);
+ mBuffer.asFloatBuffer().put(floats);
+ } else {
+ throw new RuntimeException(
+ "Trying to sync to an allocation with an unsupported element id: "
+ + getElementId());
+ }
+ }
+
+ @Override
+ public Object lock(int accessType) {
+ return mBuffer.rewind();
+ }
+
+ @Override
+ public void unlock() {
+ mBuffer.rewind();
+ }
+
+ @Override
+ public int getType() {
+ return BACKING_BYTEBUFFER;
+ }
+
+ @Override
+ public boolean shouldCache() {
+ return true;
+ }
+
+ @Override
+ public void destroy() {
+ mBuffer = null;
+ }
+
+ @Override
+ public int getSize() {
+ return mBuffer.remaining();
+ }
+
+ }
+
+ @TargetApi(11)
+ static class AllocationBacking extends Backing {
+
+ private final RenderScript mRenderScript;
+ private Allocation mAllocation = null;
+
+ public AllocationBacking(RenderScript renderScript) {
+ mRenderScript = renderScript;
+ }
+
+ @Override
+ public void allocate(FrameType frameType) {
+ assertCompatible(frameType);
+
+ Element element = null;
+ switch (frameType.getElementId()) {
+ case FrameType.ELEMENT_RGBA8888:
+ element = Element.RGBA_8888(mRenderScript);
+ break;
+ case FrameType.ELEMENT_FLOAT32:
+ element = Element.F32(mRenderScript);
+ break;
+ }
+ Type.Builder imageTypeBuilder = new Type.Builder(mRenderScript, element);
+ imageTypeBuilder.setX(mDimensions.length >= 1 ? mDimensions[0] : 1);
+ imageTypeBuilder.setY(mDimensions.length == 2 ? mDimensions[1] : 1);
+ Type imageType = imageTypeBuilder.create();
+
+ mAllocation = Allocation.createTyped(mRenderScript, imageType);
+ }
+
+ @Override
+ public int readAccess() {
+ return ACCESS_ALLOCATION;
+ }
+
+ @Override
+ public int writeAccess() {
+ return ACCESS_ALLOCATION;
+ }
+
+ @Override
+ public boolean requiresGpu() {
+ return false;
+ }
+
+ @Override
+ public void syncTo(Backing backing) {
+ int access = backing.readAccess();
+ if ((access & ACCESS_TEXTURE) != 0) {
+ RenderTarget target = (RenderTarget) backing.lock(ACCESS_RENDERTARGET);
+ ByteBuffer pixels = ByteBuffer.allocateDirect(getSize());
+ GLToolbox.readTarget(target, pixels, mDimensions[0], mDimensions[1]);
+ mAllocation.copyFrom(pixels.array());
+ } else if ((access & ACCESS_BITMAP) != 0) {
+ Bitmap bitmap = (Bitmap) backing.lock(ACCESS_BITMAP);
+ mAllocation.copyFrom(bitmap);
+ } else if ((access & ACCESS_BYTES) != 0) {
+ ByteBuffer buffer = (ByteBuffer) backing.lock(ACCESS_BYTES);
+ if (buffer.order() != ByteOrder.nativeOrder()) {
+ throw new RuntimeException(
+ "Trying to sync to the ByteBufferBacking with non-native byte order!");
+ }
+ byte[] bytes;
+ if (buffer.hasArray()) {
+ bytes = buffer.array();
+ } else {
+ bytes = new byte[getSize()];
+ buffer.get(bytes);
+ buffer.rewind();
+ }
+ mAllocation.copyFromUnchecked(bytes);
+ } else {
+ throw new RuntimeException("Cannot sync allocation backing!");
+ }
+ backing.unlock();
+ mIsDirty = false;
+ }
+
+ @Override
+ public Object lock(int accessType) {
+ return mAllocation;
+ }
+
+ @Override
+ public void unlock() {
+ }
+
+ @Override
+ public int getType() {
+ return BACKING_ALLOCATION;
+ }
+
+ @Override
+ public boolean shouldCache() {
+ return true;
+ }
+
+ @Override
+ public void destroy() {
+ if (mAllocation != null) {
+ mAllocation.destroy();
+ mAllocation = null;
+ }
+ }
+
+ @Override
+ public int getSize() {
+ int elementCount = 1;
+ for (int dim : mDimensions) {
+ elementCount *= dim;
+ }
+ return getElementSize() * elementCount;
+ }
+
+ public static boolean isSupported() {
+ return Build.VERSION.SDK_INT >= 11;
+ }
+
+ private void assertCompatible(FrameType type) {
+ // TODO: consider adding support for other data types.
+ if (type.getElementId() != FrameType.ELEMENT_RGBA8888
+ && type.getElementId() != FrameType.ELEMENT_FLOAT32) {
+ throw new RuntimeException(
+ "Cannot allocate allocation with a non-RGBA or non-float data type!");
+ }
+ if (mDimensions == null || mDimensions.length > 2) {
+ throw new RuntimeException(
+ "Cannot create an allocation with more than 2 dimensions!");
+ }
+ }
+
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BranchFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BranchFilter.java
new file mode 100644
index 0000000000000..6e7c014f9f557
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BranchFilter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterpacks.base;
+
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.Frame;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.InputPort;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.OutputPort;
+import androidx.media.filterfw.Signature;
+
+public final class BranchFilter extends Filter {
+
+ private boolean mSynchronized = true;
+
+ public BranchFilter(MffContext context, String name) {
+ super(context, name);
+ }
+
+ public BranchFilter(MffContext context, String name, boolean synced) {
+ super(context, name);
+ mSynchronized = synced;
+ }
+
+ @Override
+ public Signature getSignature() {
+ return new Signature()
+ .addInputPort("input", Signature.PORT_REQUIRED, FrameType.any())
+ .addInputPort("synchronized", Signature.PORT_OPTIONAL,FrameType.single(boolean.class))
+ .disallowOtherInputs();
+ }
+
+ @Override
+ public void onInputPortOpen(InputPort port) {
+ if (port.getName().equals("input")) {
+ for (OutputPort outputPort : getConnectedOutputPorts()) {
+ port.attachToOutputPort(outputPort);
+ }
+ } else if (port.getName().equals("synchronized")) {
+ port.bindToFieldNamed("mSynchronized");
+ port.setAutoPullEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onOpen() {
+ updateSynchronization();
+ }
+
+ @Override
+ protected void onProcess() {
+ Frame inputFrame = getConnectedInputPort("input").pullFrame();
+ for (OutputPort outputPort : getConnectedOutputPorts()) {
+ if (outputPort.isAvailable()) {
+ outputPort.pushFrame(inputFrame);
+ }
+ }
+ }
+
+ private void updateSynchronization() {
+ if (mSynchronized) {
+ for (OutputPort port : getConnectedOutputPorts()) {
+ port.setWaitsUntilAvailable(true);
+ }
+ } else {
+ for (OutputPort port : getConnectedOutputPorts()) {
+ port.setWaitsUntilAvailable(false);
+ }
+ }
+ }
+
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BrightnessFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BrightnessFilter.java
new file mode 100644
index 0000000000000..5a707768e183f
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BrightnessFilter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterpacks.image;
+
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.FrameImage2D;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.ImageShader;
+import androidx.media.filterfw.InputPort;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.OutputPort;
+import androidx.media.filterfw.Signature;
+
+public class BrightnessFilter extends Filter {
+
+ private float mBrightness = 1.0f;
+ private ImageShader mShader;
+
+ private static final String mBrightnessShader =
+ "precision mediump float;\n" +
+ "uniform sampler2D tex_sampler_0;\n" +
+ "uniform float brightness;\n" +
+ "varying vec2 v_texcoord;\n" +
+ "void main() {\n" +
+ " vec4 color = texture2D(tex_sampler_0, v_texcoord);\n" +
+ " if (brightness < 0.5) {\n" +
+ " gl_FragColor = color * (2.0 * brightness);\n" +
+ " } else {\n" +
+ " vec4 diff = 1.0 - color;\n" +
+ " gl_FragColor = color + diff * (2.0 * (brightness - 0.5));\n" +
+ " }\n" +
+ "}\n";
+
+
+ public BrightnessFilter(MffContext context, String name) {
+ super(context, name);
+ }
+
+ @Override
+ public Signature getSignature() {
+ FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU);
+ FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU);
+ return new Signature()
+ .addInputPort("image", Signature.PORT_REQUIRED, imageIn)
+ .addInputPort("brightness", Signature.PORT_OPTIONAL, FrameType.single(float.class))
+ .addOutputPort("image", Signature.PORT_REQUIRED, imageOut)
+ .disallowOtherPorts();
+ }
+
+ @Override
+ public void onInputPortOpen(InputPort port) {
+ if (port.getName().equals("brightness")) {
+ port.bindToFieldNamed("mBrightness");
+ port.setAutoPullEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onPrepare() {
+ mShader = new ImageShader(mBrightnessShader);
+ }
+
+ @Override
+ protected void onProcess() {
+ OutputPort outPort = getConnectedOutputPort("image");
+ FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D();
+ int[] dim = inputImage.getDimensions();
+ FrameImage2D outputImage = outPort.fetchAvailableFrame(dim).asFrameImage2D();
+ mShader.setUniformValue("brightness", mBrightness);
+ mShader.process(inputImage, outputImage);
+ outPort.pushFrame(outputImage);
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CameraStreamer.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CameraStreamer.java
new file mode 100644
index 0000000000000..d1642fdc53be9
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CameraStreamer.java
@@ -0,0 +1,1906 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.PreviewCallback;
+import android.media.CamcorderProfile;
+import android.media.MediaRecorder;
+import android.opengl.GLES20;
+import android.os.Build.VERSION;
+import android.util.Log;
+import android.view.Display;
+import android.view.Surface;
+import android.view.SurfaceView;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Vector;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.microedition.khronos.egl.EGLContext;
+
+/**
+ * The CameraStreamer streams Frames from a camera to connected clients.
+ *
+ * There is one centralized CameraStreamer object per MffContext, and only one stream can be
+ * active at any time. The CameraStreamer acts as a Camera "server" that streams frames to any
+ * number of connected clients. Typically, these are CameraSource filters that are part of a
+ * graph, but other clients can be written as well.
+ */
+public class CameraStreamer {
+
+ /** Camera Facing: Don't Care: Picks any available camera. */
+ public static final int FACING_DONTCARE = 0;
+ /** Camera Facing: Front: Use the front facing camera. */
+ public static final int FACING_FRONT = 1;
+ /** Camera Facing: Back: Use the rear facing camera. */
+ public static final int FACING_BACK = 2;
+
+ /** How long the streamer should wait to acquire the camera before giving up. */
+ public static long MAX_CAMERA_WAIT_TIME = 5;
+
+ /**
+ * The global camera lock, that is closed when the camera is acquired by any CameraStreamer,
+ * and opened when a streamer is done using the camera.
+ */
+ static ReentrantLock mCameraLock = new ReentrantLock();
+
+ /** The Camera thread that grabs frames from the camera */
+ private CameraRunnable mCameraRunner = null;
+
+ private abstract class CamFrameHandler {
+ protected int mCameraWidth;
+ protected int mCameraHeight;
+ protected int mOutWidth;
+ protected int mOutHeight;
+ protected CameraRunnable mRunner;
+
+ /** Map of GLSL shaders (one for each target context) */
+ protected HashMap mTargetShaders
+ = new HashMap();
+
+ /** Map of target textures (one for each target context) */
+ protected HashMap mTargetTextures
+ = new HashMap();
+
+ /** Map of set of clients (one for each target context) */
+ protected HashMap> mContextClients
+ = new HashMap>();
+
+ /** List of clients that are consuming camera frames. */
+ protected Vector mClients = new Vector();
+
+ public void initWithRunner(CameraRunnable camRunner) {
+ mRunner = camRunner;
+ }
+
+ public void setCameraSize(int width, int height) {
+ mCameraWidth = width;
+ mCameraHeight = height;
+ }
+
+ public void registerClient(FrameClient client) {
+ EGLContext context = RenderTarget.currentContext();
+ Set clientTargets = clientsForContext(context);
+ clientTargets.add(client);
+ mClients.add(client);
+ onRegisterClient(client, context);
+ }
+
+ public void unregisterClient(FrameClient client) {
+ EGLContext context = RenderTarget.currentContext();
+ Set clientTargets = clientsForContext(context);
+ clientTargets.remove(client);
+ if (clientTargets.isEmpty()) {
+ onCleanupContext(context);
+ }
+ mClients.remove(client);
+ }
+
+ public abstract void setupServerFrame();
+ public abstract void updateServerFrame();
+ public abstract void grabFrame(FrameImage2D targetFrame);
+ public abstract void release();
+
+ public void onUpdateCameraOrientation(int orientation) {
+ if (orientation % 180 != 0) {
+ mOutWidth = mCameraHeight;
+ mOutHeight = mCameraWidth;
+ } else {
+ mOutWidth = mCameraWidth;
+ mOutHeight = mCameraHeight;
+ }
+ }
+
+ protected Set clientsForContext(EGLContext context) {
+ Set clients = mContextClients.get(context);
+ if (clients == null) {
+ clients = new HashSet();
+ mContextClients.put(context, clients);
+ }
+ return clients;
+ }
+
+ protected void onRegisterClient(FrameClient client, EGLContext context) {
+ }
+
+ protected void onCleanupContext(EGLContext context) {
+ TextureSource texture = mTargetTextures.get(context);
+ ImageShader shader = mTargetShaders.get(context);
+ if (texture != null) {
+ texture.release();
+ mTargetTextures.remove(context);
+ }
+ if (shader != null) {
+ mTargetShaders.remove(context);
+ }
+ }
+
+ protected TextureSource textureForContext(EGLContext context) {
+ TextureSource texture = mTargetTextures.get(context);
+ if (texture == null) {
+ texture = createClientTexture();
+ mTargetTextures.put(context, texture);
+ }
+ return texture;
+ }
+
+ protected ImageShader shaderForContext(EGLContext context) {
+ ImageShader shader = mTargetShaders.get(context);
+ if (shader == null) {
+ shader = createClientShader();
+ mTargetShaders.put(context, shader);
+ }
+ return shader;
+ }
+
+ protected ImageShader createClientShader() {
+ return null;
+ }
+
+ protected TextureSource createClientTexture() {
+ return null;
+ }
+
+ public boolean isFrontMirrored() {
+ return true;
+ }
+ }
+
+ // Jellybean (and later) back-end
+ @TargetApi(16)
+ private class CamFrameHandlerJB extends CamFrameHandlerICS {
+
+ @Override
+ public void setupServerFrame() {
+ setupPreviewTexture(mRunner.mCamera);
+ }
+
+ @Override
+ public synchronized void updateServerFrame() {
+ updateSurfaceTexture();
+ informClients();
+ }
+
+ @Override
+ public synchronized void grabFrame(FrameImage2D targetFrame) {
+ TextureSource targetTex = TextureSource.newExternalTexture();
+ ImageShader copyShader = shaderForContext(RenderTarget.currentContext());
+ if (targetTex == null || copyShader == null) {
+ throw new RuntimeException("Attempting to grab camera frame from unknown "
+ + "thread: " + Thread.currentThread() + "!");
+ }
+ mPreviewSurfaceTexture.attachToGLContext(targetTex.getTextureId());
+ updateTransform(copyShader);
+ updateShaderTargetRect(copyShader);
+ targetFrame.resize(new int[] { mOutWidth, mOutHeight });
+ copyShader.process(targetTex,
+ targetFrame.lockRenderTarget(),
+ mOutWidth,
+ mOutHeight);
+ targetFrame.setTimestamp(mPreviewSurfaceTexture.getTimestamp());
+ targetFrame.unlock();
+ mPreviewSurfaceTexture.detachFromGLContext();
+ targetTex.release();
+ }
+
+ @Override
+ protected void updateShaderTargetRect(ImageShader shader) {
+ if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) {
+ shader.setTargetRect(1f, 1f, -1f, -1f);
+ } else {
+ shader.setTargetRect(0f, 1f, 1f, -1f);
+ }
+ }
+
+ @Override
+ protected void setupPreviewTexture(Camera camera) {
+ super.setupPreviewTexture(camera);
+ mPreviewSurfaceTexture.detachFromGLContext();
+ }
+
+ protected void updateSurfaceTexture() {
+ mPreviewSurfaceTexture.attachToGLContext(mPreviewTexture.getTextureId());
+ mPreviewSurfaceTexture.updateTexImage();
+ mPreviewSurfaceTexture.detachFromGLContext();
+ }
+
+ protected void informClients() {
+ synchronized (mClients) {
+ for (FrameClient client : mClients) {
+ client.onCameraFrameAvailable();
+ }
+ }
+ }
+ }
+
+ // ICS (and later) back-end
+ @TargetApi(15)
+ private class CamFrameHandlerICS extends CamFrameHandler {
+
+ protected static final String mCopyShaderSource =
+ "#extension GL_OES_EGL_image_external : require\n" +
+ "precision mediump float;\n" +
+ "uniform samplerExternalOES tex_sampler_0;\n" +
+ "varying vec2 v_texcoord;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(tex_sampler_0, v_texcoord);\n" +
+ "}\n";
+
+ /** The camera transform matrix */
+ private float[] mCameraTransform = new float[16];
+
+ /** The texture the camera streams to */
+ protected TextureSource mPreviewTexture = null;
+ protected SurfaceTexture mPreviewSurfaceTexture = null;
+
+ /** Map of target surface textures (one for each target context) */
+ protected HashMap mTargetSurfaceTextures
+ = new HashMap();
+
+ /** Map of RenderTargets for client SurfaceTextures */
+ protected HashMap mClientRenderTargets
+ = new HashMap();
+
+ /** Server side copy shader */
+ protected ImageShader mCopyShader = null;
+
+ @Override
+ public synchronized void setupServerFrame() {
+ setupPreviewTexture(mRunner.mCamera);
+ }
+
+ @Override
+ public synchronized void updateServerFrame() {
+ mPreviewSurfaceTexture.updateTexImage();
+ distributeFrames();
+ }
+
+ @Override
+ public void onUpdateCameraOrientation(int orientation) {
+ super.onUpdateCameraOrientation(orientation);
+ mRunner.mCamera.setDisplayOrientation(orientation);
+ updateSurfaceTextureSizes();
+ }
+
+ @Override
+ public synchronized void onRegisterClient(FrameClient client, EGLContext context) {
+ final Set clientTargets = clientsForContext(context);
+
+ // Make sure we have texture, shader, and surfacetexture setup for this context.
+ TextureSource clientTex = textureForContext(context);
+ ImageShader copyShader = shaderForContext(context);
+ SurfaceTexture surfTex = surfaceTextureForContext(context);
+
+ // Listen to client-side surface texture updates
+ surfTex.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
+ @Override
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ for (FrameClient clientTarget : clientTargets) {
+ clientTarget.onCameraFrameAvailable();
+ }
+ }
+ });
+ }
+
+ @Override
+ public synchronized void grabFrame(FrameImage2D targetFrame) {
+ // Get the GL objects for the receiver's context
+ EGLContext clientContext = RenderTarget.currentContext();
+ TextureSource clientTex = textureForContext(clientContext);
+ ImageShader copyShader = shaderForContext(clientContext);
+ SurfaceTexture surfTex = surfaceTextureForContext(clientContext);
+ if (clientTex == null || copyShader == null || surfTex == null) {
+ throw new RuntimeException("Attempting to grab camera frame from unknown "
+ + "thread: " + Thread.currentThread() + "!");
+ }
+
+ // Copy from client ST to client tex
+ surfTex.updateTexImage();
+ targetFrame.resize(new int[] { mOutWidth, mOutHeight });
+ copyShader.process(clientTex,
+ targetFrame.lockRenderTarget(),
+ mOutWidth,
+ mOutHeight);
+
+ targetFrame.setTimestamp(mPreviewSurfaceTexture.getTimestamp());
+ targetFrame.unlock();
+ }
+
+ @Override
+ public synchronized void release() {
+ if (mPreviewTexture != null) {
+ mPreviewTexture.release();
+ mPreviewTexture = null;
+ }
+ if (mPreviewSurfaceTexture != null) {
+ mPreviewSurfaceTexture.release();
+ mPreviewSurfaceTexture = null;
+ }
+ }
+
+ @Override
+ protected ImageShader createClientShader() {
+ return new ImageShader(mCopyShaderSource);
+ }
+
+ @Override
+ protected TextureSource createClientTexture() {
+ return TextureSource.newExternalTexture();
+ }
+
+ protected void distributeFrames() {
+ updateTransform(getCopyShader());
+ updateShaderTargetRect(getCopyShader());
+
+ for (SurfaceTexture clientTexture : mTargetSurfaceTextures.values()) {
+ RenderTarget clientTarget = renderTargetFor(clientTexture);
+ clientTarget.focus();
+ getCopyShader().process(mPreviewTexture,
+ clientTarget,
+ mOutWidth,
+ mOutHeight);
+ GLToolbox.checkGlError("distribute frames");
+ clientTarget.swapBuffers();
+ }
+ }
+
+ protected RenderTarget renderTargetFor(SurfaceTexture surfaceTex) {
+ RenderTarget target = mClientRenderTargets.get(surfaceTex);
+ if (target == null) {
+ target = RenderTarget.currentTarget().forSurfaceTexture(surfaceTex);
+ mClientRenderTargets.put(surfaceTex, target);
+ }
+ return target;
+ }
+
+ protected void setupPreviewTexture(Camera camera) {
+ if (mPreviewTexture == null) {
+ mPreviewTexture = TextureSource.newExternalTexture();
+ }
+ if (mPreviewSurfaceTexture == null) {
+ mPreviewSurfaceTexture = new SurfaceTexture(mPreviewTexture.getTextureId());
+ try {
+ camera.setPreviewTexture(mPreviewSurfaceTexture);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not bind camera surface texture: " +
+ e.getMessage() + "!");
+ }
+ mPreviewSurfaceTexture.setOnFrameAvailableListener(mOnCameraFrameListener);
+ }
+ }
+
+ protected ImageShader getCopyShader() {
+ if (mCopyShader == null) {
+ mCopyShader = new ImageShader(mCopyShaderSource);
+ }
+ return mCopyShader;
+ }
+
+ protected SurfaceTexture surfaceTextureForContext(EGLContext context) {
+ SurfaceTexture surfTex = mTargetSurfaceTextures.get(context);
+ if (surfTex == null) {
+ TextureSource texture = textureForContext(context);
+ if (texture != null) {
+ surfTex = new SurfaceTexture(texture.getTextureId());
+ surfTex.setDefaultBufferSize(mOutWidth, mOutHeight);
+ mTargetSurfaceTextures.put(context, surfTex);
+ }
+ }
+ return surfTex;
+ }
+
+ protected void updateShaderTargetRect(ImageShader shader) {
+ if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) {
+ shader.setTargetRect(1f, 0f, -1f, 1f);
+ } else {
+ shader.setTargetRect(0f, 0f, 1f, 1f);
+ }
+ }
+
+ protected synchronized void updateSurfaceTextureSizes() {
+ for (SurfaceTexture clientTexture : mTargetSurfaceTextures.values()) {
+ clientTexture.setDefaultBufferSize(mOutWidth, mOutHeight);
+ }
+ }
+
+ protected void updateTransform(ImageShader shader) {
+ mPreviewSurfaceTexture.getTransformMatrix(mCameraTransform);
+ shader.setSourceTransform(mCameraTransform);
+ }
+
+ @Override
+ protected void onCleanupContext(EGLContext context) {
+ super.onCleanupContext(context);
+ SurfaceTexture surfaceTex = mTargetSurfaceTextures.get(context);
+ if (surfaceTex != null) {
+ surfaceTex.release();
+ mTargetSurfaceTextures.remove(context);
+ }
+ }
+
+ protected SurfaceTexture.OnFrameAvailableListener mOnCameraFrameListener =
+ new SurfaceTexture.OnFrameAvailableListener() {
+ @Override
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ mRunner.signalNewFrame();
+ }
+ };
+ }
+
+ // Gingerbread (and later) back-end
+ @TargetApi(9)
+ private final class CamFrameHandlerGB extends CamFrameHandler {
+
+ private SurfaceView mSurfaceView;
+ private byte[] mFrameBufferFront;
+ private byte[] mFrameBufferBack;
+ private boolean mWriteToBack = true;
+ private float[] mTargetCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f };
+ final Object mBufferLock = new Object();
+
+ private String mNV21ToRGBAFragment =
+ "precision mediump float;\n" +
+ "\n" +
+ "uniform sampler2D tex_sampler_0;\n" +
+ "varying vec2 v_y_texcoord;\n" +
+ "varying vec2 v_vu_texcoord;\n" +
+ "varying vec2 v_pixcoord;\n" +
+ "\n" +
+ "vec3 select(vec4 yyyy, vec4 vuvu, int s) {\n" +
+ " if (s == 0) {\n" +
+ " return vec3(yyyy.r, vuvu.g, vuvu.r);\n" +
+ " } else if (s == 1) {\n" +
+ " return vec3(yyyy.g, vuvu.g, vuvu.r);\n" +
+ " } else if (s == 2) {\n" +
+ " return vec3(yyyy.b, vuvu.a, vuvu.b);\n" +
+ " } else {\n" +
+ " return vec3(yyyy.a, vuvu.a, vuvu.b);\n" +
+ " }\n" +
+ "}\n" +
+ "\n" +
+ "vec3 yuv2rgb(vec3 yuv) {\n" +
+ " mat4 conversion = mat4(1.0, 0.0, 1.402, -0.701,\n" +
+ " 1.0, -0.344, -0.714, 0.529,\n" +
+ " 1.0, 1.772, 0.0, -0.886,\n" +
+ " 0, 0, 0, 0);" +
+ " return (vec4(yuv, 1.0) * conversion).rgb;\n" +
+ "}\n" +
+ "\n" +
+ "void main() {\n" +
+ " vec4 yyyy = texture2D(tex_sampler_0, v_y_texcoord);\n" +
+ " vec4 vuvu = texture2D(tex_sampler_0, v_vu_texcoord);\n" +
+ " int s = int(mod(floor(v_pixcoord.x), 4.0));\n" +
+ " vec3 yuv = select(yyyy, vuvu, s);\n" +
+ " vec3 rgb = yuv2rgb(yuv);\n" +
+ " gl_FragColor = vec4(rgb, 1.0);\n" +
+ "}";
+
+ private String mNV21ToRGBAVertex =
+ "attribute vec4 a_position;\n" +
+ "attribute vec2 a_y_texcoord;\n" +
+ "attribute vec2 a_vu_texcoord;\n" +
+ "attribute vec2 a_pixcoord;\n" +
+ "varying vec2 v_y_texcoord;\n" +
+ "varying vec2 v_vu_texcoord;\n" +
+ "varying vec2 v_pixcoord;\n" +
+ "void main() {\n" +
+ " gl_Position = a_position;\n" +
+ " v_y_texcoord = a_y_texcoord;\n" +
+ " v_vu_texcoord = a_vu_texcoord;\n" +
+ " v_pixcoord = a_pixcoord;\n" +
+ "}\n";
+
+ private byte[] readBuffer() {
+ synchronized (mBufferLock) {
+ return mWriteToBack ? mFrameBufferFront : mFrameBufferBack;
+ }
+ }
+
+ private byte[] writeBuffer() {
+ synchronized (mBufferLock) {
+ return mWriteToBack ? mFrameBufferBack : mFrameBufferFront;
+ }
+ }
+
+ private synchronized void swapBuffers() {
+ synchronized (mBufferLock) {
+ mWriteToBack = !mWriteToBack;
+ }
+ }
+
+ private PreviewCallback mPreviewCallback = new PreviewCallback() {
+
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ swapBuffers();
+ camera.addCallbackBuffer(writeBuffer());
+ mRunner.signalNewFrame();
+ }
+
+ };
+
+ @Override
+ public void setupServerFrame() {
+ checkCameraDimensions();
+ Camera camera = mRunner.mCamera;
+ int bufferSize = mCameraWidth * (mCameraHeight + mCameraHeight/2);
+ mFrameBufferFront = new byte[bufferSize];
+ mFrameBufferBack = new byte[bufferSize];
+ camera.addCallbackBuffer(writeBuffer());
+ camera.setPreviewCallbackWithBuffer(mPreviewCallback);
+ SurfaceView previewDisplay = getPreviewDisplay();
+ if (previewDisplay != null) {
+ try {
+ camera.setPreviewDisplay(previewDisplay.getHolder());
+ } catch (IOException e) {
+ throw new RuntimeException("Could not start camera with given preview " +
+ "display!");
+ }
+ }
+ }
+
+ private void checkCameraDimensions() {
+ if (mCameraWidth % 4 != 0) {
+ throw new RuntimeException("Camera width must be a multiple of 4!");
+ } else if (mCameraHeight % 2 != 0) {
+ throw new RuntimeException("Camera height must be a multiple of 2!");
+ }
+ }
+
+ @Override
+ public void updateServerFrame() {
+ // Server frame has been updated already, simply inform clients here.
+ informClients();
+ }
+
+ @Override
+ public void grabFrame(FrameImage2D targetFrame) {
+ EGLContext clientContext = RenderTarget.currentContext();
+
+ // Copy camera data to the client YUV texture
+ TextureSource clientTex = textureForContext(clientContext);
+ int texWidth = mCameraWidth / 4;
+ int texHeight = mCameraHeight + mCameraHeight / 2;
+ synchronized(mBufferLock) { // Don't swap buffers while we are reading
+ ByteBuffer pixels = ByteBuffer.wrap(readBuffer());
+ clientTex.allocateWithPixels(pixels, texWidth, texHeight);
+ }
+ clientTex.setParameter(GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
+ clientTex.setParameter(GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
+
+ // Setup the YUV-2-RGBA shader
+ ImageShader transferShader = shaderForContext(clientContext);
+ transferShader.setTargetCoords(mTargetCoords);
+ updateShaderPixelSize(transferShader);
+
+ // Convert pixels into target frame
+ targetFrame.resize(new int[] { mOutWidth, mOutHeight });
+ transferShader.process(clientTex,
+ targetFrame.lockRenderTarget(),
+ mOutWidth,
+ mOutHeight);
+ targetFrame.unlock();
+ }
+
+ @Override
+ public void onUpdateCameraOrientation(int orientation) {
+ super.onUpdateCameraOrientation(orientation);
+ if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) {
+ switch (orientation) {
+ case 0:
+ mTargetCoords = new float[] { 1f, 0f, 0f, 0f, 1f, 1f, 0f, 1f };
+ break;
+ case 90:
+ mTargetCoords = new float[] { 0f, 0f, 0f, 1f, 1f, 0f, 1f, 1f };
+ break;
+ case 180:
+ mTargetCoords = new float[] { 0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f };
+ break;
+ case 270:
+ mTargetCoords = new float[] { 1f, 1f, 1f, 0f, 0f, 1f, 0f, 0f };
+ break;
+ }
+ } else {
+ switch (orientation) {
+ case 0:
+ mTargetCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f };
+ break;
+ case 90:
+ mTargetCoords = new float[] { 1f, 0f, 1f, 1f, 0f, 0f, 0f, 1f };
+ break;
+ case 180:
+ mTargetCoords = new float[] { 1f, 1f, 0f, 1f, 1f, 0f, 0f, 0f };
+ break;
+ case 270:
+ mTargetCoords = new float[] { 0f, 1f, 0f, 0f, 1f, 1f, 1f, 0f };
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void release() {
+ mFrameBufferBack = null;
+ mFrameBufferFront = null;
+ }
+
+ @Override
+ public boolean isFrontMirrored() {
+ return false;
+ }
+
+ @Override
+ protected ImageShader createClientShader() {
+ ImageShader shader = new ImageShader(mNV21ToRGBAVertex, mNV21ToRGBAFragment);
+ // TODO: Make this a VBO
+ float[] yCoords = new float[] {
+ 0f, 0f,
+ 1f, 0f,
+ 0f, 2f / 3f,
+ 1f, 2f / 3f };
+ float[] uvCoords = new float[] {
+ 0f, 2f / 3f,
+ 1f, 2f / 3f,
+ 0f, 1f,
+ 1f, 1f };
+ shader.setAttributeValues("a_y_texcoord", yCoords, 2);
+ shader.setAttributeValues("a_vu_texcoord", uvCoords, 2);
+ return shader;
+ }
+
+ @Override
+ protected TextureSource createClientTexture() {
+ TextureSource texture = TextureSource.newTexture();
+ texture.setParameter(GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
+ texture.setParameter(GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
+ return texture;
+ }
+
+ private void updateShaderPixelSize(ImageShader shader) {
+ float[] pixCoords = new float[] {
+ 0f, 0f,
+ mCameraWidth, 0f,
+ 0f, mCameraHeight,
+ mCameraWidth, mCameraHeight };
+ shader.setAttributeValues("a_pixcoord", pixCoords, 2);
+ }
+
+ private SurfaceView getPreviewDisplay() {
+ if (mSurfaceView == null) {
+ mSurfaceView = mRunner.getContext().getDummySurfaceView();
+ }
+ return mSurfaceView;
+ }
+
+ private void informClients() {
+ synchronized (mClients) {
+ for (FrameClient client : mClients) {
+ client.onCameraFrameAvailable();
+ }
+ }
+ }
+ }
+
+ private static class State {
+ public static final int STATE_RUNNING = 1;
+ public static final int STATE_STOPPED = 2;
+ public static final int STATE_HALTED = 3;
+
+ private AtomicInteger mCurrent = new AtomicInteger(STATE_STOPPED);
+
+ public int current() {
+ return mCurrent.get();
+ }
+
+ public void set(int newState) {
+ mCurrent.set(newState);
+ }
+ }
+
+ private static class Event {
+ public static final int START = 1;
+ public static final int FRAME = 2;
+ public static final int STOP = 3;
+ public static final int HALT = 4;
+ public static final int RESTART = 5;
+ public static final int UPDATE = 6;
+ public static final int TEARDOWN = 7;
+
+ public int code;
+
+ public Event(int code) {
+ this.code = code;
+ }
+ }
+
+ private final class CameraRunnable implements Runnable {
+
+ /** On slower devices the event queue can easily fill up. We bound the queue to this. */
+ private final static int MAX_EVENTS = 32;
+
+ /** The runner's state */
+ private State mState = new State();
+
+ /** The CameraRunner's event queue */
+ private LinkedBlockingQueue mEventQueue = new LinkedBlockingQueue(MAX_EVENTS);
+
+ /** The requested FPS */
+ private int mRequestedFramesPerSec = 30;
+
+ /** The actual FPS */
+ private int mActualFramesPerSec = 0;
+
+ /** The requested preview width and height */
+ private int mRequestedPreviewWidth = 640;
+ private int mRequestedPreviewHeight = 480;
+
+ /** The requested picture width and height */
+ private int mRequestedPictureWidth = 640;
+ private int mRequestedPictureHeight = 480;
+
+ /** The actual camera width and height */
+ private int[] mActualDims = null;
+
+ /** The requested facing */
+ private int mRequestedFacing = FACING_DONTCARE;
+
+ /** The actual facing */
+ private int mActualFacing = FACING_DONTCARE;
+
+ /** Whether to horizontally flip the front facing camera */
+ private boolean mFlipFront = true;
+
+ /** The display the camera streamer is bound to. */
+ private Display mDisplay = null;
+
+ /** The camera and screen orientation. */
+ private int mCamOrientation = 0;
+ private int mOrientation = -1;
+
+ /** The camera rotation (used for capture). */
+ private int mCamRotation = 0;
+
+ /** The camera flash mode */
+ private String mFlashMode = Camera.Parameters.FLASH_MODE_OFF;
+
+ /** The camera object */
+ private Camera mCamera = null;
+
+ private MediaRecorder mRecorder = null;
+
+ /** The ID of the currently used camera */
+ int mCamId = 0;
+
+ /** The platform-dependent camera frame handler. */
+ private CamFrameHandler mCamFrameHandler = null;
+
+ /** The set of camera listeners. */
+ private Set mCamListeners = new HashSet();
+
+ private ReentrantLock mCameraReadyLock = new ReentrantLock(true);
+ // mCameraReady condition is used when waiting for the camera getting ready.
+ private Condition mCameraReady = mCameraReadyLock.newCondition();
+ // external camera lock used to provide the capability of external camera access.
+ private ExternalCameraLock mExternalCameraLock = new ExternalCameraLock();
+
+ private RenderTarget mRenderTarget;
+ private MffContext mContext;
+
+ /**
+ * This provides the capability of locking and unlocking from different threads.
+ * The thread will wait until the lock state is idle. Any thread can wake up
+ * a waiting thread by calling unlock (i.e. signal), provided that unlock
+ * are called using the same context when lock was called. Using context prevents
+ * from rogue usage of unlock.
+ */
+ private class ExternalCameraLock {
+ public static final int IDLE = 0;
+ public static final int IN_USE = 1;
+ private int mLockState = IDLE;
+ private Object mLockContext;
+ private final ReentrantLock mLock = new ReentrantLock(true);
+ private final Condition mInUseLockCondition= mLock.newCondition();
+
+ public boolean lock(Object context) {
+ if (context == null) {
+ throw new RuntimeException("Null context when locking");
+ }
+ mLock.lock();
+ if (mLockState == IN_USE) {
+ try {
+ mInUseLockCondition.await();
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+ mLockState = IN_USE;
+ mLockContext = context;
+ mLock.unlock();
+ return true;
+ }
+
+ public void unlock(Object context) {
+ mLock.lock();
+ if (mLockState != IN_USE) {
+ throw new RuntimeException("Not in IN_USE state");
+ }
+ if (context != mLockContext) {
+ throw new RuntimeException("Lock is not owned by this context");
+ }
+ mLockState = IDLE;
+ mLockContext = null;
+ mInUseLockCondition.signal();
+ mLock.unlock();
+ }
+ }
+
+ public CameraRunnable(MffContext context) {
+ mContext = context;
+ createCamFrameHandler();
+ mCamFrameHandler.initWithRunner(this);
+ launchThread();
+ }
+
+ public MffContext getContext() {
+ return mContext;
+ }
+
+ public void loop() {
+ while (true) {
+ try {
+ Event event = nextEvent();
+ if (event == null) continue;
+ switch (event.code) {
+ case Event.START:
+ onStart();
+ break;
+ case Event.STOP:
+ onStop();
+ break;
+ case Event.FRAME:
+ onFrame();
+ break;
+ case Event.HALT:
+ onHalt();
+ break;
+ case Event.RESTART:
+ onRestart();
+ break;
+ case Event.UPDATE:
+ onUpdate();
+ break;
+ case Event.TEARDOWN:
+ onTearDown();
+ break;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ loop();
+ }
+
+ public void signalNewFrame() {
+ pushEvent(Event.FRAME, false);
+ }
+
+ public void pushEvent(int eventId, boolean required) {
+ try {
+ if (required) {
+ mEventQueue.put(new Event(eventId));
+ } else {
+ mEventQueue.offer(new Event(eventId));
+ }
+ } catch (InterruptedException e) {
+ // We should never get here (as we do not limit capacity in the queue), but if
+ // we do, we log an error.
+ Log.e("CameraStreamer", "Dropping event " + eventId + "!");
+ }
+ }
+
+ public void launchThread() {
+ Thread cameraThread = new Thread(this);
+ cameraThread.start();
+ }
+
+ @Deprecated
+ public Camera getCamera() {
+ synchronized (mState) {
+ return mCamera;
+ }
+ }
+
+ public Camera lockCamera(Object context) {
+ mExternalCameraLock.lock(context);
+ /**
+ * since lockCamera can happen right after closeCamera,
+ * the camera handle can be null, wait until valid handle
+ * is acquired.
+ */
+ while (mCamera == null) {
+ mExternalCameraLock.unlock(context);
+ mCameraReadyLock.lock();
+ try {
+ mCameraReady.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Condition interrupted", e);
+ }
+ mCameraReadyLock.unlock();
+ mExternalCameraLock.lock(context);
+ }
+ return mCamera;
+ }
+
+ public void unlockCamera(Object context) {
+ mExternalCameraLock.unlock(context);
+ }
+
+ public int getCurrentCameraId() {
+ synchronized (mState) {
+ return mCamId;
+ }
+ }
+
+ public boolean isRunning() {
+ return mState.current() != State.STATE_STOPPED;
+ }
+
+ public void addListener(CameraListener listener) {
+ synchronized (mCamListeners) {
+ mCamListeners.add(listener);
+ }
+ }
+
+ public void removeListener(CameraListener listener) {
+ synchronized (mCamListeners) {
+ mCamListeners.remove(listener);
+ }
+ }
+
+ public synchronized void bindToDisplay(Display display) {
+ mDisplay = display;
+ }
+
+ public synchronized void setDesiredPreviewSize(int width, int height) {
+ if (width != mRequestedPreviewWidth || height != mRequestedPreviewHeight) {
+ mRequestedPreviewWidth = width;
+ mRequestedPreviewHeight = height;
+ onParamsUpdated();
+ }
+ }
+
+ public synchronized void setDesiredPictureSize(int width, int height) {
+ if (width != mRequestedPictureWidth || height != mRequestedPictureHeight) {
+ mRequestedPictureWidth = width;
+ mRequestedPictureHeight = height;
+ onParamsUpdated();
+ }
+ }
+
+ public synchronized void setDesiredFrameRate(int fps) {
+ if (fps != mRequestedFramesPerSec) {
+ mRequestedFramesPerSec = fps;
+ onParamsUpdated();
+ }
+ }
+
+ public synchronized void setFacing(int facing) {
+ if (facing != mRequestedFacing) {
+ switch (facing) {
+ case FACING_DONTCARE:
+ case FACING_FRONT:
+ case FACING_BACK:
+ mRequestedFacing = facing;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown facing value '" + facing
+ + "' passed to setFacing!");
+ }
+ onParamsUpdated();
+ }
+ }
+
+ public synchronized void setFlipFrontCamera(boolean flipFront) {
+ if (mFlipFront != flipFront) {
+ mFlipFront = flipFront;
+ }
+ }
+
+ public synchronized void setFlashMode(String flashMode) {
+ if (!flashMode.equals(mFlashMode)) {
+ mFlashMode = flashMode;
+ onParamsUpdated();
+ }
+ }
+
+ public synchronized int getCameraFacing() {
+ return mActualFacing;
+ }
+
+ public synchronized int getCameraRotation() {
+ return mCamRotation;
+ }
+
+ public synchronized boolean supportsHardwareFaceDetection() {
+ //return mCamFrameHandler.supportsHardwareFaceDetection();
+ // TODO
+ return true;
+ }
+
+ public synchronized int getCameraWidth() {
+ return (mActualDims != null) ? mActualDims[0] : 0;
+ }
+
+ public synchronized int getCameraHeight() {
+ return (mActualDims != null) ? mActualDims[1] : 0;
+ }
+
+ public synchronized int getCameraFrameRate() {
+ return mActualFramesPerSec;
+ }
+
+ public synchronized String getFlashMode() {
+ return mCamera.getParameters().getFlashMode();
+ }
+
+ public synchronized boolean canStart() {
+ // If we can get a camera id without error we should be able to start.
+ try {
+ getCameraId();
+ } catch (RuntimeException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean grabFrame(FrameImage2D targetFrame) {
+ // Make sure we stay in state running while we are grabbing the frame.
+ synchronized (mState) {
+ if (mState.current() != State.STATE_RUNNING) {
+ return false;
+ }
+ // we may not have the camera ready, this might happen when in the middle
+ // of switching camera.
+ if (mCamera == null) {
+ return false;
+ }
+ mCamFrameHandler.grabFrame(targetFrame);
+ return true;
+ }
+ }
+
+ public CamFrameHandler getCamFrameHandler() {
+ return mCamFrameHandler;
+ }
+
+ private void onParamsUpdated() {
+ pushEvent(Event.UPDATE, true);
+ }
+
+ private Event nextEvent() {
+ try {
+ return mEventQueue.take();
+ } catch (InterruptedException e) {
+ // Ignore and keep going.
+ Log.w("GraphRunner", "Event queue processing was interrupted.");
+ return null;
+ }
+ }
+
+ private void onStart() {
+ if (mState.current() == State.STATE_STOPPED) {
+ mState.set(State.STATE_RUNNING);
+ getRenderTarget().focus();
+ openCamera();
+ }
+ }
+
+ private void onStop() {
+ if (mState.current() == State.STATE_RUNNING) {
+ closeCamera();
+ RenderTarget.focusNone();
+ }
+ // Set state to stop (halted becomes stopped).
+ mState.set(State.STATE_STOPPED);
+ }
+
+ private void onHalt() {
+ // Only halt if running. Stopped overrides halt.
+ if (mState.current() == State.STATE_RUNNING) {
+ closeCamera();
+ RenderTarget.focusNone();
+ mState.set(State.STATE_HALTED);
+ }
+ }
+
+ private void onRestart() {
+ // Only restart if halted
+ if (mState.current() == State.STATE_HALTED) {
+ mState.set(State.STATE_RUNNING);
+ getRenderTarget().focus();
+ openCamera();
+ }
+ }
+
+ private void onUpdate() {
+ if (mState.current() == State.STATE_RUNNING) {
+ pushEvent(Event.STOP, true);
+ pushEvent(Event.START, true);
+ }
+ }
+ private void onFrame() {
+ if (mState.current() == State.STATE_RUNNING) {
+ updateRotation();
+ mCamFrameHandler.updateServerFrame();
+ }
+ }
+
+ private void onTearDown() {
+ if (mState.current() == State.STATE_STOPPED) {
+ // Remove all listeners. This will release their resources
+ for (CameraListener listener : mCamListeners) {
+ removeListener(listener);
+ }
+ mCamListeners.clear();
+ } else {
+ Log.e("CameraStreamer", "Could not tear-down CameraStreamer as camera still "
+ + "seems to be running!");
+ }
+ }
+
+ private void createCamFrameHandler() {
+ // TODO: For now we simply assert that OpenGL is supported. Later on, we should add
+ // a CamFrameHandler that does not depend on OpenGL.
+ getContext().assertOpenGLSupported();
+ if (VERSION.SDK_INT >= 16) {
+ mCamFrameHandler = new CamFrameHandlerJB();
+ } else if (VERSION.SDK_INT >= 15) {
+ mCamFrameHandler = new CamFrameHandlerICS();
+ } else {
+ mCamFrameHandler = new CamFrameHandlerGB();
+ }
+ }
+
+ private void updateRotation() {
+ if (mDisplay != null) {
+ updateDisplayRotation(mDisplay.getRotation());
+ }
+ }
+
+ private synchronized void updateDisplayRotation(int rotation) {
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ onUpdateOrientation(0);
+ break;
+ case Surface.ROTATION_90:
+ onUpdateOrientation(90);
+ break;
+ case Surface.ROTATION_180:
+ onUpdateOrientation(180);
+ break;
+ case Surface.ROTATION_270:
+ onUpdateOrientation(270);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported display rotation constant! Use "
+ + "one of the Surface.ROTATION_ constants!");
+ }
+ }
+
+ private RenderTarget getRenderTarget() {
+ if (mRenderTarget == null) {
+ mRenderTarget = RenderTarget.newTarget(1, 1);
+ }
+ return mRenderTarget;
+ }
+
+ private void updateCamera() {
+ synchronized (mState) {
+ mCamId = getCameraId();
+ updateCameraOrientation(mCamId);
+ mCamera = Camera.open(mCamId);
+ initCameraParameters();
+ }
+ }
+
+ private void updateCameraOrientation(int camId) {
+ CameraInfo cameraInfo = new CameraInfo();
+ Camera.getCameraInfo(camId, cameraInfo);
+ mCamOrientation = cameraInfo.orientation;
+ mOrientation = -1; // Forces recalculation to match display
+ mActualFacing = (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT)
+ ? FACING_FRONT
+ : FACING_BACK;
+ }
+
+ private int getCameraId() {
+ int camCount = Camera.getNumberOfCameras();
+ if (camCount == 0) {
+ throw new RuntimeException("Device does not have any cameras!");
+ } else if (mRequestedFacing == FACING_DONTCARE) {
+ // Simply return first camera if mRequestedFacing is don't care
+ return 0;
+ }
+
+ // Attempt to find requested camera
+ boolean useFrontCam = (mRequestedFacing == FACING_FRONT);
+ CameraInfo cameraInfo = new CameraInfo();
+ for (int i = 0; i < camCount; ++i) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if ((cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) == useFrontCam) {
+ return i;
+ }
+ }
+ throw new RuntimeException("Could not find a camera facing (" + mRequestedFacing
+ + ")!");
+ }
+
+ private void initCameraParameters() {
+ Camera.Parameters params = mCamera.getParameters();
+
+ // Find closest preview size
+ mActualDims =
+ findClosestPreviewSize(mRequestedPreviewWidth, mRequestedPreviewHeight, params);
+ mCamFrameHandler.setCameraSize(mActualDims[0], mActualDims[1]);
+ params.setPreviewSize(mActualDims[0], mActualDims[1]);
+ // Find closest picture size
+ int[] dims =
+ findClosestPictureSize(mRequestedPictureWidth, mRequestedPictureHeight, params);
+ params.setPictureSize(dims[0], dims[1]);
+
+ // Find closest FPS
+ int closestRange[] = findClosestFpsRange(mRequestedFramesPerSec, params);
+ params.setPreviewFpsRange(closestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
+ closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
+
+ // Set flash mode (if supported)
+ if (params.getFlashMode() != null) {
+ params.setFlashMode(mFlashMode);
+ }
+
+ mCamera.setParameters(params);
+ }
+
+ private int[] findClosestPreviewSize(int width, int height, Camera.Parameters parameters) {
+ List previewSizes = parameters.getSupportedPreviewSizes();
+ return findClosestSizeFromList(width, height, previewSizes);
+ }
+
+ private int[] findClosestPictureSize(int width, int height, Camera.Parameters parameters) {
+ List pictureSizes = parameters.getSupportedPictureSizes();
+ return findClosestSizeFromList(width, height, pictureSizes);
+ }
+
+ private int[] findClosestSizeFromList(int width, int height, List sizes) {
+ int closestWidth = -1;
+ int closestHeight = -1;
+ int smallestWidth = sizes.get(0).width;
+ int smallestHeight = sizes.get(0).height;
+ for (Camera.Size size : sizes) {
+ // Best match defined as not being larger in either dimension than
+ // the requested size, but as close as possible. The below isn't a
+ // stable selection (reording the size list can give different
+ // results), but since this is a fallback nicety, that's acceptable.
+ if ( size.width <= width &&
+ size.height <= height &&
+ size.width >= closestWidth &&
+ size.height >= closestHeight) {
+ closestWidth = size.width;
+ closestHeight = size.height;
+ }
+ if ( size.width < smallestWidth &&
+ size.height < smallestHeight) {
+ smallestWidth = size.width;
+ smallestHeight = size.height;
+ }
+ }
+ if (closestWidth == -1) {
+ // Requested size is smaller than any listed size; match with smallest possible
+ closestWidth = smallestWidth;
+ closestHeight = smallestHeight;
+ }
+ int[] closestSize = {closestWidth, closestHeight};
+ return closestSize;
+ }
+
+ private int[] findClosestFpsRange(int fps, Camera.Parameters params) {
+ List supportedFpsRanges = params.getSupportedPreviewFpsRange();
+ int[] closestRange = supportedFpsRanges.get(0);
+ int fpsk = fps * 1000;
+ int minDiff = 1000000;
+ for (int[] range : supportedFpsRanges) {
+ int low = range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
+ int high = range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
+ if (low <= fpsk && high >= fpsk) {
+ int diff = (fpsk - low) + (high - fpsk);
+ if (diff < minDiff) {
+ closestRange = range;
+ minDiff = diff;
+ }
+ }
+ }
+ mActualFramesPerSec = closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX] / 1000;
+ return closestRange;
+ }
+
+ private void onUpdateOrientation(int orientation) {
+ // First we calculate the camera rotation.
+ int rotation = (mActualFacing == FACING_FRONT)
+ ? (mCamOrientation + orientation) % 360
+ : (mCamOrientation - orientation + 360) % 360;
+ if (rotation != mCamRotation) {
+ synchronized (this) {
+ mCamRotation = rotation;
+ }
+ }
+
+ // We compensate for mirroring in the orientation. This differs from the rotation,
+ // where we are invariant to mirroring.
+ int fixedOrientation = rotation;
+ if (mActualFacing == FACING_FRONT && mCamFrameHandler.isFrontMirrored()) {
+ fixedOrientation = (360 - rotation) % 360; // compensate the mirror
+ }
+ if (mOrientation != fixedOrientation) {
+ mOrientation = fixedOrientation;
+ mCamFrameHandler.onUpdateCameraOrientation(mOrientation);
+ }
+ }
+
+ private void openCamera() {
+ // Acquire lock for camera
+ try {
+ if (!mCameraLock.tryLock(MAX_CAMERA_WAIT_TIME, TimeUnit.SECONDS)) {
+ throw new RuntimeException("Timed out while waiting to acquire camera!");
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting to acquire camera!");
+ }
+
+ // Make sure external entities are not holding camera. We need to hold the lock until
+ // the preview is started again.
+ Object lockContext = new Object();
+ mExternalCameraLock.lock(lockContext);
+
+ // Need to synchronize this as many of the member values are modified during setup.
+ synchronized (this) {
+ updateCamera();
+ updateRotation();
+ mCamFrameHandler.setupServerFrame();
+ }
+
+ mCamera.startPreview();
+
+ // Inform listeners
+ synchronized (mCamListeners) {
+ for (CameraListener listener : mCamListeners) {
+ listener.onCameraOpened(CameraStreamer.this);
+ }
+ }
+ mExternalCameraLock.unlock(lockContext);
+ // New camera started
+ mCameraReadyLock.lock();
+ mCameraReady.signal();
+ mCameraReadyLock.unlock();
+ }
+
+ /**
+ * Creates an instance of MediaRecorder to be used for the streamer.
+ * User should call the functions in the following sequence:
+ * {@link #createRecorder}
+ * {@link #startRecording}
+ * {@link #stopRecording}
+ * {@link #releaseRecorder}
+ * @param outputPath the output video path for the recorder
+ * @param profile the recording {@link CamcorderProfile} which has parameters indicating
+ * the resolution, quality etc.
+ */
+ public void createRecorder(String outputPath, CamcorderProfile profile) {
+ lockCamera(this);
+ mCamera.unlock();
+ if (mRecorder != null) {
+ mRecorder.release();
+ }
+ mRecorder = new MediaRecorder();
+ mRecorder.setCamera(mCamera);
+ mRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
+ mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+ mRecorder.setProfile(profile);
+ mRecorder.setOutputFile(outputPath);
+ try {
+ mRecorder.prepare();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Starts recording video using the created MediaRecorder object
+ */
+ public void startRecording() {
+ if (mRecorder == null) {
+ throw new RuntimeException("No recorder created");
+ }
+ mRecorder.start();
+ }
+
+ /**
+ * Stops recording video
+ */
+ public void stopRecording() {
+ if (mRecorder == null) {
+ throw new RuntimeException("No recorder created");
+ }
+ mRecorder.stop();
+ }
+
+ /**
+ * Release the resources held by the MediaRecorder, call this after done recording.
+ */
+ public void releaseRecorder() {
+ if (mRecorder == null) {
+ throw new RuntimeException("No recorder created");
+ }
+ mRecorder.release();
+ mRecorder = null;
+ mCamera.lock();
+ unlockCamera(this);
+ }
+
+ private void closeCamera() {
+ Object lockContext = new Object();
+ mExternalCameraLock.lock(lockContext);
+ if (mCamera != null) {
+ mCamera.stopPreview();
+ mCamera.release();
+ mCamera = null;
+ }
+ mCameraLock.unlock();
+ mCamFrameHandler.release();
+ mExternalCameraLock.unlock(lockContext);
+ // Inform listeners
+ synchronized (mCamListeners) {
+ for (CameraListener listener : mCamListeners) {
+ listener.onCameraClosed(CameraStreamer.this);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * The frame-client callback interface.
+ * FrameClients, that wish to receive Frames from the camera must implement this callback
+ * method.
+ * Note, that this method is called on the Camera server thread. However, the
+ * {@code getLatestFrame()} method must be called from the client thread.
+ */
+ public static interface FrameClient {
+ public void onCameraFrameAvailable();
+ }
+
+ /**
+ * The CameraListener callback interface.
+ * This interface allows observers to monitor the CameraStreamer and respond to stream open
+ * and close events.
+ */
+ public static interface CameraListener {
+ /**
+ * Called when the camera is opened and begins producing frames.
+ * This is also called when settings have changed that caused the camera to be reopened.
+ */
+ public void onCameraOpened(CameraStreamer camera);
+
+ /**
+ * Called when the camera is closed and stops producing frames.
+ */
+ public void onCameraClosed(CameraStreamer camera);
+ }
+
+ /**
+ * Manually update the display rotation.
+ * You do not need to call this, if the camera is bound to a display, or your app does not
+ * support multiple orientations.
+ */
+ public void updateDisplayRotation(int rotation) {
+ mCameraRunner.updateDisplayRotation(rotation);
+ }
+
+ /**
+ * Bind the camera to your Activity's display.
+ * Use this, if your Activity supports multiple display orientation, and you would like the
+ * camera to update accordingly when the orientation is changed.
+ */
+ public void bindToDisplay(Display display) {
+ mCameraRunner.bindToDisplay(display);
+ }
+
+ /**
+ * Sets the desired preview size.
+ * Note that the actual width and height may vary.
+ *
+ * @param width The desired width of the preview camera stream.
+ * @param height The desired height of the preview camera stream.
+ */
+ public void setDesiredPreviewSize(int width, int height) {
+ mCameraRunner.setDesiredPreviewSize(width, height);
+ }
+
+ /**
+ * Sets the desired picture size.
+ * Note that the actual width and height may vary.
+ *
+ * @param width The desired picture width.
+ * @param height The desired picture height.
+ */
+ public void setDesiredPictureSize(int width, int height) {
+ mCameraRunner.setDesiredPictureSize(width, height);
+ }
+
+ /**
+ * Sets the desired camera frame-rate.
+ * Note, that the actual frame-rate may vary.
+ *
+ * @param fps The desired FPS.
+ */
+ public void setDesiredFrameRate(int fps) {
+ mCameraRunner.setDesiredFrameRate(fps);
+ }
+
+ /**
+ * Sets the camera facing direction.
+ *
+ * Specify {@code FACING_DONTCARE} (default) if you would like the CameraStreamer to choose
+ * the direction. When specifying any other direction be sure to first check whether the
+ * device supports the desired facing.
+ *
+ * @param facing The desired camera facing direction.
+ */
+ public void setFacing(int facing) {
+ mCameraRunner.setFacing(facing);
+ }
+
+ /**
+ * Set whether to flip the camera image horizontally when using the front facing camera.
+ */
+ public void setFlipFrontCamera(boolean flipFront) {
+ mCameraRunner.setFlipFrontCamera(flipFront);
+ }
+
+ /**
+ * Sets the camera flash mode.
+ *
+ * This must be one of the String constants defined in the Camera.Parameters class.
+ *
+ * @param flashMode A String constant specifying the flash mode.
+ */
+ public void setFlashMode(String flashMode) {
+ mCameraRunner.setFlashMode(flashMode);
+ }
+
+ /**
+ * Returns the current flash mode.
+ *
+ * This returns the currently running camera's flash-mode, or NULL if flash modes are not
+ * supported on that camera.
+ *
+ * @return The flash mode String, or NULL if flash modes are not supported.
+ */
+ public String getFlashMode() {
+ return mCameraRunner.getFlashMode();
+ }
+
+ /**
+ * Get the actual camera facing.
+ * Returns 0 if actual facing is not yet known.
+ */
+ public int getCameraFacing() {
+ return mCameraRunner.getCameraFacing();
+ }
+
+ /**
+ * Get the current camera rotation.
+ *
+ * Use this rotation if you want to snap pictures from the camera and need to rotate the
+ * picture to be up-right.
+ *
+ * @return the current camera rotation.
+ */
+ public int getCameraRotation() {
+ return mCameraRunner.getCameraRotation();
+ }
+
+ /**
+ * Specifies whether or not the camera supports hardware face detection.
+ * @return true, if the camera supports hardware face detection.
+ */
+ public boolean supportsHardwareFaceDetection() {
+ return mCameraRunner.supportsHardwareFaceDetection();
+ }
+
+ /**
+ * Returns the camera facing that is chosen when DONT_CARE is specified.
+ * Returns 0 if neither a front nor back camera could be found.
+ */
+ public static int getDefaultFacing() {
+ int camCount = Camera.getNumberOfCameras();
+ if (camCount == 0) {
+ return 0;
+ } else {
+ CameraInfo cameraInfo = new CameraInfo();
+ Camera.getCameraInfo(0, cameraInfo);
+ return (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT)
+ ? FACING_FRONT
+ : FACING_BACK;
+ }
+ }
+
+ /**
+ * Get the actual camera width.
+ * Returns 0 if actual width is not yet known.
+ */
+ public int getCameraWidth() {
+ return mCameraRunner.getCameraWidth();
+ }
+
+ /**
+ * Get the actual camera height.
+ * Returns 0 if actual height is not yet known.
+ */
+ public int getCameraHeight() {
+ return mCameraRunner.getCameraHeight();
+ }
+
+ /**
+ * Get the actual camera frame-rate.
+ * Returns 0 if actual frame-rate is not yet known.
+ */
+ public int getCameraFrameRate() {
+ return mCameraRunner.getCameraFrameRate();
+ }
+
+ /**
+ * Returns true if the camera can be started at this point.
+ */
+ public boolean canStart() {
+ return mCameraRunner.canStart();
+ }
+
+ /**
+ * Returns true if the camera is currently running.
+ */
+ public boolean isRunning() {
+ return mCameraRunner.isRunning();
+ }
+
+ /**
+ * Starts the camera.
+ */
+ public void start() {
+ mCameraRunner.pushEvent(Event.START, true);
+ }
+
+ /**
+ * Stops the camera.
+ */
+ public void stop() {
+ mCameraRunner.pushEvent(Event.STOP, true);
+ }
+
+ /**
+ * Stops the camera and waits until it is completely closed. Generally, this should not be
+ * called in the UI thread, but may be necessary if you need the camera to be closed before
+ * performing subsequent steps.
+ */
+ public void stopAndWait() {
+ mCameraRunner.pushEvent(Event.STOP, true);
+ try {
+ if (!mCameraLock.tryLock(MAX_CAMERA_WAIT_TIME, TimeUnit.SECONDS)) {
+ Log.w("CameraStreamer", "Time-out waiting for camera to close!");
+ }
+ } catch (InterruptedException e) {
+ Log.w("CameraStreamer", "Interrupted while waiting for camera to close!");
+ }
+ mCameraLock.unlock();
+ }
+
+ /**
+ * Registers a listener to handle camera state changes.
+ */
+ public void addListener(CameraListener listener) {
+ mCameraRunner.addListener(listener);
+ }
+
+ /**
+ * Unregisters a listener to handle camera state changes.
+ */
+ public void removeListener(CameraListener listener) {
+ mCameraRunner.removeListener(listener);
+ }
+
+ /**
+ * Registers the frame-client with the camera.
+ * This MUST be called from the client thread!
+ */
+ public void registerClient(FrameClient client) {
+ mCameraRunner.getCamFrameHandler().registerClient(client);
+ }
+
+ /**
+ * Unregisters the frame-client with the camera.
+ * This MUST be called from the client thread!
+ */
+ public void unregisterClient(FrameClient client) {
+ mCameraRunner.getCamFrameHandler().unregisterClient(client);
+ }
+
+ /**
+ * Gets the latest camera frame for the client.
+ *
+ * This must be called from the same thread as the {@link #registerClient(FrameClient)} call!
+ * The frame passed in will be resized by the camera streamer to fit the camera frame.
+ * Returns false if the frame could not be grabbed. This may happen if the camera has been
+ * closed in the meantime, and its resources let go.
+ *
+ * @return true, if the frame was grabbed successfully.
+ */
+ public boolean getLatestFrame(FrameImage2D targetFrame) {
+ return mCameraRunner.grabFrame(targetFrame);
+ }
+
+ /**
+ * Expose the underlying android.hardware.Camera object.
+ * Use the returned object with care: some camera functions may break the functionality
+ * of CameraStreamer.
+ * @return the Camera object.
+ */
+ @Deprecated
+ public Camera getCamera() {
+ return mCameraRunner.getCamera();
+ }
+
+ /**
+ * Obtain access to the underlying android.hardware.Camera object.
+ * This grants temporary access to the internal Camera handle. Once you are done using the
+ * handle you must call {@link #unlockCamera(Object)}. While you are holding the Camera,
+ * it will not be modified or released by the CameraStreamer. The Camera object return is
+ * guaranteed to have the preview running.
+ *
+ * The CameraStreamer does not account for changes you make to the Camera. That is, if you
+ * change the Camera unexpectedly this may cause unintended behavior by the streamer.
+ *
+ * Note that the returned object may be null. This can happen when the CameraStreamer is not
+ * running, or is just transitioning to another Camera, such as during a switch from front to
+ * back Camera.
+ * @param context an object used as a context for locking and unlocking. lockCamera and
+ * unlockCamera should use the same context object.
+ * @return The Camera object.
+ */
+ public Camera lockCamera(Object context) {
+ return mCameraRunner.lockCamera(context);
+ }
+
+ /**
+ * Release the acquire Camera object.
+ * @param context the context object that used when lockCamera is called.
+ */
+ public void unlockCamera(Object context) {
+ mCameraRunner.unlockCamera(context);
+ }
+
+ /**
+ * Creates an instance of MediaRecorder to be used for the streamer.
+ * User should call the functions in the following sequence:
+ * {@link #createRecorder}
+ * {@link #startRecording}
+ * {@link #stopRecording}
+ * {@link #releaseRecorder}
+ * @param path the output video path for the recorder
+ * @param profile the recording {@link CamcorderProfile} which has parameters indicating
+ * the resolution, quality etc.
+ */
+ public void createRecorder(String path, CamcorderProfile profile) {
+ mCameraRunner.createRecorder(path, profile);
+ }
+
+ public void releaseRecorder() {
+ mCameraRunner.releaseRecorder();
+ }
+
+ public void startRecording() {
+ mCameraRunner.startRecording();
+ }
+
+ public void stopRecording() {
+ mCameraRunner.stopRecording();
+ }
+
+ /**
+ * Retrieve the ID of the currently used camera.
+ * @return the ID of the currently used camera.
+ */
+ public int getCameraId() {
+ return mCameraRunner.getCurrentCameraId();
+ }
+
+ /**
+ * @return The number of cameras available for streaming on this device.
+ */
+ public static int getNumberOfCameras() {
+ // Currently, this is just the number of cameras that are available on the device.
+ return Camera.getNumberOfCameras();
+ }
+
+ CameraStreamer(MffContext context) {
+ mCameraRunner = new CameraRunnable(context);
+ }
+
+ /** Halt is like stop, but may be resumed using restart(). */
+ void halt() {
+ mCameraRunner.pushEvent(Event.HALT, true);
+ }
+
+ /** Restart starts the camera only if previously halted. */
+ void restart() {
+ mCameraRunner.pushEvent(Event.RESTART, true);
+ }
+
+ static boolean requireDummySurfaceView() {
+ return VERSION.SDK_INT < 15;
+ }
+
+ void tearDown() {
+ mCameraRunner.pushEvent(Event.TEARDOWN, true);
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorSpace.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorSpace.java
new file mode 100644
index 0000000000000..f2bfe08e71660
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorSpace.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media.filterfw;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Utility functions to convert between color-spaces.
+ *
+ * Currently these methods are all CPU based native methods. These could be updated in the future
+ * to provide other implementations.
+ */
+public class ColorSpace {
+
+ /**
+ * Convert YUV420-Planer data to RGBA8888.
+ *
+ * The input data is expected to be laid out in 3 planes. The width x height Y plane, followed
+ * by the U and V planes, where each chroma value corresponds to a 2x2 luminance value block.
+ * YUV to RGB conversion is done using the ITU-R BT.601 transformation. The output buffer must
+ * be large enough to hold the data, and the dimensions must be multiples of 2.
+ *
+ * @param input data encoded in YUV420-Planar.
+ * @param output buffer to hold RGBA8888 data.
+ * @param width the width of the image (must be a multiple of 2)
+ * @param height the height of the image (must be a multiple of 2)
+ */
+ public static void convertYuv420pToRgba8888(
+ ByteBuffer input, ByteBuffer output, int width, int height) {
+ expectInputSize(input, (3 * width * height) / 2);
+ expectOutputSize(output, width * height * 4);
+ nativeYuv420pToRgba8888(input, output, width, height);
+ }
+
+ /**
+ * Convert ARGB8888 to RGBA8888.
+ *
+ * The input data is expected to be encoded in 8-bit interleaved ARGB channels. The output
+ * buffer must be large enough to hold the data. The output buffer may be the same as the
+ * input buffer.
+ *
+ * @param input data encoded in ARGB8888.
+ * @param output buffer to hold RGBA8888 data.
+ * @param width the width of the image
+ * @param height the height of the image
+ */
+ public static void convertArgb8888ToRgba8888(
+ ByteBuffer input, ByteBuffer output, int width, int height) {
+ expectInputSize(input, width * height * 4);
+ expectOutputSize(output, width * height * 4);
+ nativeArgb8888ToRgba8888(input, output, width, height);
+ }
+
+ /**
+ * Convert RGBA8888 to HSVA8888.
+ *
+ * The input data is expected to be encoded in 8-bit interleaved RGBA channels. The output
+ * buffer must be large enough to hold the data. The output buffer may be the same as the
+ * input buffer.
+ *
+ * @param input data encoded in RGBA8888.
+ * @param output buffer to hold HSVA8888 data.
+ * @param width the width of the image
+ * @param height the height of the image
+ */
+ public static void convertRgba8888ToHsva8888(
+ ByteBuffer input, ByteBuffer output, int width, int height) {
+ expectInputSize(input, width * height * 4);
+ expectOutputSize(output, width * height * 4);
+ nativeRgba8888ToHsva8888(input, output, width, height);
+ }
+
+ /**
+ * Convert RGBA8888 to YCbCrA8888.
+ *
+ * The input data is expected to be encoded in 8-bit interleaved RGBA channels. The output
+ * buffer must be large enough to hold the data. The output buffer may be the same as the
+ * input buffer.
+ *
+ * @param input data encoded in RGBA8888.
+ * @param output buffer to hold YCbCrA8888 data.
+ * @param width the width of the image
+ * @param height the height of the image
+ */
+ public static void convertRgba8888ToYcbcra8888(
+ ByteBuffer input, ByteBuffer output, int width, int height) {
+ expectInputSize(input, width * height * 4);
+ expectOutputSize(output, width * height * 4);
+ nativeRgba8888ToYcbcra8888(input, output, width, height);
+ }
+
+ private static void expectInputSize(ByteBuffer input, int expectedSize) {
+ if (input.remaining() < expectedSize) {
+ throw new IllegalArgumentException("Input buffer's size does not fit given width "
+ + "and height! Expected: " + expectedSize + ", Got: " + input.remaining()
+ + ".");
+ }
+ }
+
+ private static void expectOutputSize(ByteBuffer output, int expectedSize) {
+ if (output.remaining() < expectedSize) {
+ throw new IllegalArgumentException("Output buffer's size does not fit given width "
+ + "and height! Expected: " + expectedSize + ", Got: " + output.remaining()
+ + ".");
+ }
+ }
+
+ private static native void nativeYuv420pToRgba8888(
+ ByteBuffer input, ByteBuffer output, int width, int height);
+
+ private static native void nativeArgb8888ToRgba8888(
+ ByteBuffer input, ByteBuffer output, int width, int height);
+
+ private static native void nativeRgba8888ToHsva8888(
+ ByteBuffer input, ByteBuffer output, int width, int height);
+
+ private static native void nativeRgba8888ToYcbcra8888(
+ ByteBuffer input, ByteBuffer output, int width, int height);
+
+ static {
+ System.loadLibrary("smartcamera_jni");
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorfulnessFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorfulnessFilter.java
new file mode 100644
index 0000000000000..5bdf4af3d7e25
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorfulnessFilter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Extract histogram from image.
+
+package androidx.media.filterpacks.colorspace;
+
+import androidx.media.filterfw.FrameValue;
+
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.Frame;
+import androidx.media.filterfw.FrameBuffer2D;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.OutputPort;
+import androidx.media.filterfw.Signature;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+/**
+ * ColorfulnessFilter takes in a particular Chroma histogram generated by NewChromaHistogramFilter
+ * and compute the colorfulness based on the entropy in Hue space.
+ */
+public final class ColorfulnessFilter extends Filter {
+
+ public ColorfulnessFilter(MffContext context, String name) {
+ super(context, name);
+ }
+
+ @Override
+ public Signature getSignature() {
+ FrameType dataIn = FrameType.buffer2D(FrameType.ELEMENT_FLOAT32);
+ return new Signature()
+ .addInputPort("histogram", Signature.PORT_REQUIRED, dataIn)
+ .addOutputPort("score", Signature.PORT_REQUIRED, FrameType.single(float.class))
+ .disallowOtherPorts();
+ }
+
+ @Override
+ protected void onProcess() {
+ FrameBuffer2D histogramFrame =
+ getConnectedInputPort("histogram").pullFrame().asFrameBuffer2D();
+ ByteBuffer byteBuffer = histogramFrame.lockBytes(Frame.MODE_READ);
+ byteBuffer.order(ByteOrder.nativeOrder());
+ FloatBuffer histogramBuffer = byteBuffer.asFloatBuffer();
+ histogramBuffer.rewind();
+
+ // Create a hue histogram from hue-saturation histogram
+ int hueBins = histogramFrame.getWidth();
+ int saturationBins = histogramFrame.getHeight() - 1;
+ float[] hueHistogram = new float[hueBins];
+ float total = 0;
+ for (int r = 0; r < saturationBins; ++r) {
+ float weight = (float) Math.pow(2, r);
+ for (int c = 0; c < hueBins; c++) {
+ float value = histogramBuffer.get() * weight;
+ hueHistogram[c] += value;
+ total += value;
+ }
+ }
+ float colorful = 0f;
+ for (int c = 0; c < hueBins; ++c) {
+ float value = hueHistogram[c] / total;
+ if (value > 0f) {
+ colorful -= value * ((float) Math.log(value));
+ }
+ }
+
+ colorful /= Math.log(2);
+
+ histogramFrame.unlock();
+ OutputPort outPort = getConnectedOutputPort("score");
+ FrameValue frameValue = outPort.fetchAvailableFrame(null).asFrameValue();
+ frameValue.setValue(colorful);
+ outPort.pushFrame(frameValue);
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CropFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CropFilter.java
new file mode 100644
index 0000000000000..91fe21c57d307
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CropFilter.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterpacks.transform;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.util.FloatMath;
+
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.FrameImage2D;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.ImageShader;
+import androidx.media.filterfw.InputPort;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.OutputPort;
+import androidx.media.filterfw.Signature;
+import androidx.media.filterfw.geometry.Quad;
+
+public class CropFilter extends Filter {
+
+ private Quad mCropRect = Quad.fromRect(0f, 0f, 1f, 1f);
+ private int mOutputWidth = 0;
+ private int mOutputHeight = 0;
+ private ImageShader mShader;
+ private boolean mUseMipmaps = false;
+ private FrameImage2D mPow2Frame = null;
+
+ public CropFilter(MffContext context, String name) {
+ super(context, name);
+ }
+
+ @Override
+ public Signature getSignature() {
+ FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU);
+ FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU);
+ return new Signature()
+ .addInputPort("image", Signature.PORT_REQUIRED, imageIn)
+ .addInputPort("cropRect", Signature.PORT_REQUIRED, FrameType.single(Quad.class))
+ .addInputPort("outputWidth", Signature.PORT_OPTIONAL, FrameType.single(int.class))
+ .addInputPort("outputHeight", Signature.PORT_OPTIONAL, FrameType.single(int.class))
+ .addInputPort("useMipmaps", Signature.PORT_OPTIONAL, FrameType.single(boolean.class))
+ .addOutputPort("image", Signature.PORT_REQUIRED, imageOut)
+ .disallowOtherPorts();
+ }
+
+ @Override
+ public void onInputPortOpen(InputPort port) {
+ if (port.getName().equals("cropRect")) {
+ port.bindToFieldNamed("mCropRect");
+ port.setAutoPullEnabled(true);
+ } else if (port.getName().equals("outputWidth")) {
+ port.bindToFieldNamed("mOutputWidth");
+ port.setAutoPullEnabled(true);
+ } else if (port.getName().equals("outputHeight")) {
+ port.bindToFieldNamed("mOutputHeight");
+ port.setAutoPullEnabled(true);
+ } else if (port.getName().equals("useMipmaps")) {
+ port.bindToFieldNamed("mUseMipmaps");
+ port.setAutoPullEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onPrepare() {
+ if (isOpenGLSupported()) {
+ mShader = ImageShader.createIdentity();
+ }
+ }
+
+ @Override
+ protected void onProcess() {
+ OutputPort outPort = getConnectedOutputPort("image");
+
+ // Pull input frame
+ FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D();
+ int[] inDims = inputImage.getDimensions();
+ int[] croppedDims = { (int)FloatMath.ceil(mCropRect.xEdge().length() * inDims[0]),
+ (int)FloatMath.ceil(mCropRect.yEdge().length() * inDims[1]) };
+ int[] outDims = { getOutputWidth(croppedDims[0], croppedDims[1]),
+ getOutputHeight(croppedDims[0], croppedDims[1]) };
+ FrameImage2D outputImage = outPort.fetchAvailableFrame(outDims).asFrameImage2D();
+
+ if (isOpenGLSupported()) {
+ FrameImage2D sourceFrame;
+ Quad sourceQuad = null;
+ boolean scaleDown = (outDims[0] < croppedDims[0]) || (outDims[1] < croppedDims[1]);
+ if (scaleDown && mUseMipmaps) {
+ mPow2Frame = TransformUtils.makeMipMappedFrame(mPow2Frame, croppedDims);
+ int[] extDims = mPow2Frame.getDimensions();
+ float targetWidth = croppedDims[0] / (float)extDims[0];
+ float targetHeight = croppedDims[1] / (float)extDims[1];
+ Quad targetQuad = Quad.fromRect(0f, 0f, targetWidth, targetHeight);
+ mShader.setSourceQuad(mCropRect);
+ mShader.setTargetQuad(targetQuad);
+ mShader.process(inputImage, mPow2Frame);
+ TransformUtils.generateMipMaps(mPow2Frame);
+ sourceFrame = mPow2Frame;
+ sourceQuad = targetQuad;
+ } else {
+ sourceFrame = inputImage;
+ sourceQuad = mCropRect;
+ }
+
+ mShader.setSourceQuad(sourceQuad);
+ mShader.setTargetRect(0f, 0f, 1f, 1f);
+ mShader.process(sourceFrame, outputImage);
+ } else {
+ // Convert quads to canvas coordinate space
+ Quad sourceQuad = mCropRect.scale2(inDims[0], inDims[1]);
+ Quad targetQuad = Quad.fromRect(0f, 0f, inDims[0], inDims[1]);
+
+ // Calculate transform for crop
+ Matrix transform = Quad.getTransform(sourceQuad, targetQuad);
+ transform.postScale(outDims[0] / (float)inDims[0], outDims[1] / (float)inDims[1]);
+
+ // Create target canvas
+ Bitmap.Config config = Bitmap.Config.ARGB_8888;
+ Bitmap cropped = Bitmap.createBitmap(outDims[0], outDims[1], config);
+ Canvas canvas = new Canvas(cropped);
+
+ // Draw source bitmap into target canvas
+ Paint paint = new Paint();
+ paint.setFilterBitmap(true);
+ Bitmap sourceBitmap = inputImage.toBitmap();
+ canvas.drawBitmap(sourceBitmap, transform, paint);
+
+ // Assign bitmap to output frame
+ outputImage.setBitmap(cropped);
+ }
+
+ outPort.pushFrame(outputImage);
+ }
+
+ @Override
+ protected void onClose() {
+ if (mPow2Frame != null){
+ mPow2Frame.release();
+ mPow2Frame = null;
+ }
+ }
+
+ protected int getOutputWidth(int inWidth, int inHeight) {
+ return mOutputWidth <= 0 ? inWidth : mOutputWidth;
+ }
+
+ protected int getOutputHeight(int inWidth, int inHeight) {
+ return mOutputHeight <= 0 ? inHeight : mOutputHeight;
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Filter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Filter.java
new file mode 100644
index 0000000000000..9e2eb92bfb158
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Filter.java
@@ -0,0 +1,766 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Filters are the processing nodes of the filter graphs.
+ *
+ * Filters may have any number of input and output ports, through which the data frames flow.
+ * TODO: More documentation on filter life-cycle, port and type checking, GL and RenderScript, ...
+ */
+public abstract class Filter {
+
+ private static class State {
+ private static final int STATE_UNPREPARED = 1;
+ private static final int STATE_PREPARED = 2;
+ private static final int STATE_OPEN = 3;
+ private static final int STATE_CLOSED = 4;
+ private static final int STATE_DESTROYED = 5;
+
+ public int current = STATE_UNPREPARED;
+
+ public synchronized boolean check(int state) {
+ return current == state;
+ }
+
+ }
+
+ private final int REQUEST_FLAG_NONE = 0;
+ private final int REQUEST_FLAG_CLOSE = 1;
+
+ private String mName;
+ private MffContext mContext;
+ private FilterGraph mFilterGraph;
+
+ private State mState = new State();
+ private int mRequests = REQUEST_FLAG_NONE;
+
+ private int mMinimumAvailableInputs = 1;
+ private int mMinimumAvailableOutputs = 1;
+
+ private int mScheduleCount = 0;
+ private long mLastScheduleTime = 0;
+
+ private boolean mIsActive = true;
+ private AtomicBoolean mIsSleeping = new AtomicBoolean(false);
+
+ private long mCurrentTimestamp = Frame.TIMESTAMP_NOT_SET;
+
+ private HashMap mConnectedInputPorts = new HashMap();
+ private HashMap mConnectedOutputPorts = new HashMap();
+
+ private InputPort[] mConnectedInputPortArray = null;
+ private OutputPort[] mConnectedOutputPortArray = null;
+
+ private ArrayList mAutoReleaseFrames = new ArrayList();
+
+
+ /**
+ * Constructs a new filter.
+ * A filter is bound to a specific MffContext. Its name can be any String value, but it must
+ * be unique within the filter graph.
+ *
+ * Note that names starting with "$" are reserved for internal use, and should not be used.
+ *
+ * @param context The MffContext in which the filter will live.
+ * @param name The name of the filter.
+ */
+ protected Filter(MffContext context, String name) {
+ mName = name;
+ mContext = context;
+ }
+
+ /**
+ * Checks whether the filter class is available on this platform.
+ * Some filters may not be installed on all platforms and can therefore not be instantiated.
+ * Before instantiating a filter, check if it is available by using this method.
+ *
+ * This method uses the shared FilterFactory to check whether the filter class is available.
+ *
+ * @param filterClassName The fully qualified class name of the Filter class.
+ * @return true, if filters of the specified class name are available.
+ */
+ public static final boolean isAvailable(String filterClassName) {
+ return FilterFactory.sharedFactory().isFilterAvailable(filterClassName);
+ }
+
+ /**
+ * Returns the name of this filter.
+ *
+ * @return the name of the filter (specified during construction).
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the signature of this filter.
+ *
+ * Subclasses should override this and return their filter signature. The default
+ * implementation returns a generic signature with no constraints.
+ *
+ * This method may be called at any time.
+ *
+ * @return the Signature instance for this filter.
+ */
+ public Signature getSignature() {
+ return new Signature();
+ }
+
+ /**
+ * Returns the MffContext that the filter resides in.
+ *
+ * @return the MffContext of the filter.
+ */
+ public MffContext getContext() {
+ return mContext;
+ }
+
+ /**
+ * Returns true, if the filter is active.
+ * TODO: thread safety?
+ *
+ * @return true, if the filter is active.
+ */
+ public boolean isActive() {
+ return mIsActive;
+ }
+
+ /**
+ * Activates the current filter.
+ * Only active filters can be scheduled for execution. This method can only be called if the
+ * GraphRunner that is executing the filter is stopped or paused.
+ */
+ public void activate() {
+ assertIsPaused();
+ if (!mIsActive) {
+ mIsActive = true;
+ }
+ }
+
+ /**
+ * Deactivates the current filter.
+ * Only active filters can be scheduled for execution. This method can only be called if the
+ * GraphRunner that is executing the filter is stopped or paused.
+ */
+ public void deactivate() {
+ // TODO: Support close-on-deactivate (must happen in processing thread).
+ assertIsPaused();
+ if (mIsActive) {
+ mIsActive = false;
+ }
+ }
+
+ /**
+ * Returns the filter's set of input ports.
+ * Note that this contains only the *connected* input ports. To retrieve all
+ * input ports that this filter accepts, one has to go via the filter's Signature.
+ *
+ * @return An array containing all connected input ports.
+ */
+ public final InputPort[] getConnectedInputPorts() {
+ return mConnectedInputPortArray;
+ }
+
+ /**
+ * Returns the filter's set of output ports.
+ * Note that this contains only the *connected* output ports. To retrieve all
+ * output ports that this filter provides, one has to go via the filter's Signature.
+ *
+ * @return An array containing all connected output ports.
+ */
+ public final OutputPort[] getConnectedOutputPorts() {
+ return mConnectedOutputPortArray;
+ }
+
+ /**
+ * Returns the input port with the given name.
+ * Note that this can only access the *connected* input ports. To retrieve all
+ * input ports that this filter accepts, one has to go via the filter's Signature.
+ *
+ * @return the input port with the specified name, or null if no connected input port
+ * with this name exists.
+ */
+ public final InputPort getConnectedInputPort(String name) {
+ return mConnectedInputPorts.get(name);
+ }
+
+ /**
+ * Returns the output port with the given name.
+ * Note that this can only access the *connected* output ports. To retrieve all
+ * output ports that this filter provides, one has to go via the filter's Signature.
+ *
+ * @return the output port with the specified name, or null if no connected output port
+ * with this name exists.
+ */
+ public final OutputPort getConnectedOutputPort(String name) {
+ return mConnectedOutputPorts.get(name);
+ }
+
+ /**
+ * Called when an input port has been attached in the graph.
+ * Override this method, in case you want to be informed of any connected input ports, or make
+ * modifications to them. Note that you may not assume that any other ports have been attached
+ * already. If you have dependencies on other ports, override
+ * {@link #onInputPortOpen(InputPort)}. The default implementation does nothing.
+ *
+ * @param port The InputPort instance that was attached.
+ */
+ protected void onInputPortAttached(InputPort port) {
+ }
+
+ /**
+ * Called when an output port has been attached in the graph.
+ * Override this method, in case you want to be informed of any connected output ports, or make
+ * modifications to them. Note that you may not assume that any other ports have been attached
+ * already. If you have dependencies on other ports, override
+ * {@link #onOutputPortOpen(OutputPort)}. The default implementation does nothing.
+ *
+ * @param port The OutputPort instance that was attached.
+ */
+ protected void onOutputPortAttached(OutputPort port) {
+ }
+
+ /**
+ * Called when an input port is opened on this filter.
+ * Input ports are opened by the data produce, that is the filter that is connected to an
+ * input port. Override this if you need to make modifications to the port before processing
+ * begins. Note, that this is only called if the connected filter is scheduled. You may assume
+ * that all ports are attached when this is called.
+ *
+ * @param port The InputPort instance that was opened.
+ */
+ protected void onInputPortOpen(InputPort port) {
+ }
+
+ /**
+ * Called when an output port is opened on this filter.
+ * Output ports are opened when the filter they are attached to is opened. Override this if you
+ * need to make modifications to the port before processing begins. Note, that this is only
+ * called if the filter is scheduled. You may assume that all ports are attached when this is
+ * called.
+ *
+ * @param port The OutputPort instance that was opened.
+ */
+ protected void onOutputPortOpen(OutputPort port) {
+ }
+
+ /**
+ * Returns true, if the filter is currently open.
+ * @return true, if the filter is currently open.
+ */
+ public final boolean isOpen() {
+ return mState.check(State.STATE_OPEN);
+ }
+
+ @Override
+ public String toString() {
+ return mName + " (" + getClass().getSimpleName() + ")";
+ }
+
+ /**
+ * Called when filter is prepared.
+ * Subclasses can override this to prepare the filter for processing. This method gets called
+ * once only just before the filter is scheduled for processing the first time.
+ *
+ * @see #onTearDown()
+ */
+ protected void onPrepare() {
+ }
+
+ /**
+ * Called when the filter is opened.
+ * Subclasses can override this to perform any kind of initialization just before processing
+ * starts. This method may be called any number of times, but is always balanced with an
+ * {@link #onClose()} call.
+ *
+ * @see #onClose()
+ */
+ protected void onOpen() {
+ }
+
+ /**
+ * Called to perform processing on Frame data.
+ * This is the only method subclasses must override. It is called every time the filter is
+ * ready for processing. Typically this is when there is input data to process and available
+ * output ports, but may differ depending on the port configuration.
+ */
+ protected abstract void onProcess();
+
+ /**
+ * Called when the filter is closed.
+ * Subclasses can override this to perform any kind of post-processing steps. Processing will
+ * not resume until {@link #onOpen()} is called again. This method is only called if the filter
+ * is open.
+ *
+ * @see #onOpen()
+ */
+ protected void onClose() {
+ }
+
+ /**
+ * Called when the filter is torn down.
+ * Subclasses can override this to perform clean-up tasks just before the filter is disposed of.
+ * It is called when the filter graph that the filter belongs to is disposed.
+ *
+ * @see #onPrepare()
+ */
+ protected void onTearDown() {
+ }
+
+ /**
+ * Check if the input conditions are met in order to schedule this filter.
+ *
+ * This is used by {@link #canSchedule()} to determine if the input-port conditions given by
+ * the filter are met. Subclasses that override scheduling behavior can make use of this
+ * function.
+ *
+ * @return true, if the filter's input conditions are met.
+ */
+ protected boolean inputConditionsMet() {
+ if (mConnectedInputPortArray.length > 0) {
+ int inputFrames = 0;
+ // [Non-iterator looping]
+ for (int i = 0; i < mConnectedInputPortArray.length; ++i) {
+ if (!mConnectedInputPortArray[i].conditionsMet()) {
+ return false;
+ } else if (mConnectedInputPortArray[i].hasFrame()) {
+ ++inputFrames;
+ }
+ }
+ if (inputFrames < mMinimumAvailableInputs) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check if the output conditions are met in order to schedule this filter.
+ *
+ * This is used by {@link #canSchedule()} to determine if the output-port conditions given by
+ * the filter are met. Subclasses that override scheduling behavior can make use of this
+ * function.
+ *
+ * @return true, if the filter's output conditions are met.
+ */
+ protected boolean outputConditionsMet() {
+ if (mConnectedOutputPortArray.length > 0) {
+ int availableOutputs = 0;
+ for (int i = 0; i < mConnectedOutputPortArray.length; ++i) {
+ if (!mConnectedOutputPortArray[i].conditionsMet()) {
+ return false;
+ } else if (mConnectedOutputPortArray[i].isAvailable()) {
+ ++availableOutputs;
+ }
+ }
+ if (availableOutputs < mMinimumAvailableOutputs) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check if the Filter is in a state so that it can be scheduled.
+ *
+ * When overriding the filter's {@link #canSchedule()} method, you should never allow
+ * scheduling a filter that is not in a schedulable state. This will result in undefined
+ * behavior.
+ *
+ * @return true, if the filter is in a schedulable state.
+ */
+ protected boolean inSchedulableState() {
+ return (mIsActive && !mState.check(State.STATE_CLOSED));
+ }
+
+ /**
+ * Returns true if the filter can be currently scheduled.
+ *
+ * Filters may override this method if they depend on custom factors that determine whether
+ * they can be scheduled or not. The scheduler calls this method to determine whether or not
+ * a filter can be scheduled for execution. It does not guarantee that it will be executed.
+ * It is strongly recommended to call super's implementation to make sure your filter can be
+ * scheduled based on its state, input and output ports.
+ *
+ * @return true, if the filter can be scheduled.
+ */
+ protected boolean canSchedule() {
+ return inSchedulableState() && inputConditionsMet() && outputConditionsMet();
+ }
+
+ /**
+ * Returns the current FrameManager instance.
+ * @return the current FrameManager instance or null if there is no FrameManager set up yet.
+ */
+ protected final FrameManager getFrameManager() {
+ return mFilterGraph.mRunner != null ? mFilterGraph.mRunner.getFrameManager() : null;
+ }
+
+ /**
+ * Returns whether the GraphRunner for this filter is running.
+ *
+ * Generally, this method should not be used for performing operations that need to be carried
+ * out before running begins. Use {@link #performPreparation(Runnable)} for this.
+ *
+ * @return true, if the GraphRunner for this filter is running.
+ */
+ protected final boolean isRunning() {
+ return mFilterGraph != null && mFilterGraph.mRunner != null
+ && mFilterGraph.mRunner.isRunning();
+ }
+
+ /**
+ * Performs operations before the filter is running.
+ *
+ * Use this method when your filter requires to perform operations while the graph is not
+ * running. The filter will not be scheduled for execution until your method has completed
+ * execution.
+ */
+ protected final boolean performPreparation(Runnable runnable) {
+ synchronized (mState) {
+ if (mState.current == State.STATE_OPEN) {
+ return false;
+ } else {
+ runnable.run();
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Request that this filter be closed after the current processing step.
+ *
+ * Implementations may call this within their {@link #onProcess()} calls to indicate that the
+ * filter is done processing and wishes to be closed. After such a request the filter will be
+ * closed and no longer receive {@link #onProcess()} calls.
+ *
+ * @see #onClose()
+ * @see #onProcess()
+ */
+ protected final void requestClose() {
+ mRequests |= REQUEST_FLAG_CLOSE;
+ }
+
+ /**
+ * Sets the minimum number of input frames required to process.
+ * A filter will not be scheduled unless at least a certain number of input frames are available
+ * on the input ports. This is only relevant if the filter has input ports and is not waiting on
+ * all ports.
+ * The default value is 1.
+ *
+ * @param count the minimum number of frames required to process.
+ * @see #getMinimumAvailableInputs()
+ * @see #setMinimumAvailableOutputs(int)
+ * @see InputPort#setWaitsForFrame(boolean)
+ */
+ protected final void setMinimumAvailableInputs(int count) {
+ mMinimumAvailableInputs = count;
+ }
+
+ /**
+ * Returns the minimum number of input frames required to process this filter.
+ * The default value is 1.
+ *
+ * @return the minimum number of input frames required to process.
+ * @see #setMinimumAvailableInputs(int)
+ */
+ protected final int getMinimumAvailableInputs() {
+ return mMinimumAvailableInputs;
+ }
+
+ /**
+ * Sets the minimum number of available output ports required to process.
+ * A filter will not be scheduled unless atleast a certain number of output ports are available.
+ * This is only relevant if the filter has output ports and is not waiting on all ports. The
+ * default value is 1.
+ *
+ * @param count the minimum number of frames required to process.
+ * @see #getMinimumAvailableOutputs()
+ * @see #setMinimumAvailableInputs(int)
+ * @see OutputPort#setWaitsUntilAvailable(boolean)
+ */
+ protected final void setMinimumAvailableOutputs(int count) {
+ mMinimumAvailableOutputs = count;
+ }
+
+ /**
+ * Returns the minimum number of available outputs required to process this filter.
+ * The default value is 1.
+ *
+ * @return the minimum number of available outputs required to process.
+ * @see #setMinimumAvailableOutputs(int)
+ */
+ protected final int getMinimumAvailableOutputs() {
+ return mMinimumAvailableOutputs;
+ }
+
+ /**
+ * Puts the filter to sleep so that it is no longer scheduled.
+ * To resume scheduling the filter another thread must call wakeUp() on this filter.
+ */
+ protected final void enterSleepState() {
+ mIsSleeping.set(true);
+ }
+
+ /**
+ * Wakes the filter and resumes scheduling.
+ * This is generally called from another thread to signal that this filter should resume
+ * processing. Does nothing if filter is not sleeping.
+ */
+ protected final void wakeUp() {
+ if (mIsSleeping.getAndSet(false)) {
+ if (isRunning()) {
+ mFilterGraph.mRunner.signalWakeUp();
+ }
+ }
+ }
+
+ /**
+ * Returns whether this Filter is allowed to use OpenGL.
+ *
+ * Filters may use OpenGL if the MffContext supports OpenGL and its GraphRunner allows it.
+ *
+ * @return true, if this Filter is allowed to use OpenGL.
+ */
+ protected final boolean isOpenGLSupported() {
+ return mFilterGraph.mRunner.isOpenGLSupported();
+ }
+
+ /**
+ * Connect an output port to an input port of another filter.
+ * Connects the output port with the specified name to the input port with the specified name
+ * of the specified filter. If the input or output ports do not exist already, they are
+ * automatically created and added to the respective filter.
+ */
+ final void connect(String outputName, Filter targetFilter, String inputName) {
+ // Make sure not connected already
+ if (getConnectedOutputPort(outputName) != null) {
+ throw new RuntimeException("Attempting to connect already connected output port '"
+ + outputName + "' of filter " + this + "'!");
+ } else if (targetFilter.getConnectedInputPort(inputName) != null) {
+ throw new RuntimeException("Attempting to connect already connected input port '"
+ + inputName + "' of filter " + targetFilter + "'!");
+ }
+
+ // Establish connection
+ InputPort inputPort = targetFilter.newInputPort(inputName);
+ OutputPort outputPort = newOutputPort(outputName);
+ outputPort.setTarget(inputPort);
+
+ // Fire attachment callbacks
+ targetFilter.onInputPortAttached(inputPort);
+ onOutputPortAttached(outputPort);
+
+ // Update array of ports (which is maintained for more efficient access)
+ updatePortArrays();
+ }
+
+ final Map getConnectedInputPortMap() {
+ return mConnectedInputPorts;
+ }
+
+ final Map getConnectedOutputPortMap() {
+ return mConnectedOutputPorts;
+ }
+
+ final void execute() {
+ synchronized (mState) {
+ autoPullInputs();
+ mLastScheduleTime = SystemClock.elapsedRealtime();
+ if (mState.current == State.STATE_UNPREPARED) {
+ onPrepare();
+ mState.current = State.STATE_PREPARED;
+ }
+ if (mState.current == State.STATE_PREPARED) {
+ openPorts();
+ onOpen();
+ mState.current = State.STATE_OPEN;
+ }
+ if (mState.current == State.STATE_OPEN) {
+ onProcess();
+ if (mRequests != REQUEST_FLAG_NONE) {
+ processRequests();
+ }
+ }
+ }
+ autoReleaseFrames();
+ ++mScheduleCount;
+ }
+
+ final void performClose() {
+ synchronized (mState) {
+ if (mState.current == State.STATE_OPEN) {
+ onClose();
+ mIsSleeping.set(false);
+ mState.current = State.STATE_CLOSED;
+ mCurrentTimestamp = Frame.TIMESTAMP_NOT_SET;
+ }
+ }
+ }
+
+ final void softReset() {
+ synchronized (mState) {
+ performClose();
+ if (mState.current == State.STATE_CLOSED) {
+ mState.current = State.STATE_PREPARED;
+ }
+ }
+ }
+
+ final void performTearDown() {
+ synchronized (mState) {
+ if (mState.current == State.STATE_OPEN) {
+ throw new RuntimeException("Attempting to tear-down filter " + this + " which is "
+ + "in an open state!");
+ } else if (mState.current != State.STATE_DESTROYED
+ && mState.current != State.STATE_UNPREPARED) {
+ onTearDown();
+ mState.current = State.STATE_DESTROYED;
+ }
+ }
+ }
+
+ final void insertIntoFilterGraph(FilterGraph graph) {
+ mFilterGraph = graph;
+ updatePortArrays();
+ }
+
+ final int getScheduleCount() {
+ return mScheduleCount;
+ }
+
+ final void resetScheduleCount() {
+ mScheduleCount = 0;
+ }
+
+ final void openPorts() {
+ // Opening the output ports will open the connected input ports
+ for (OutputPort outputPort : mConnectedOutputPorts.values()) {
+ openOutputPort(outputPort);
+ }
+ }
+
+ final void addAutoReleaseFrame(Frame frame) {
+ mAutoReleaseFrames.add(frame);
+ }
+
+ final long getCurrentTimestamp() {
+ return mCurrentTimestamp;
+ }
+
+ final void onPulledFrameWithTimestamp(long timestamp) {
+ if (timestamp > mCurrentTimestamp || mCurrentTimestamp == Frame.TIMESTAMP_NOT_SET) {
+ mCurrentTimestamp = timestamp;
+ }
+ }
+
+ final void openOutputPort(OutputPort outPort) {
+ if (outPort.getQueue() == null) {
+ try {
+ FrameQueue.Builder builder = new FrameQueue.Builder();
+ InputPort inPort = outPort.getTarget();
+ outPort.onOpen(builder);
+ inPort.onOpen(builder);
+ Filter targetFilter = inPort.getFilter();
+ String queueName = mName + "[" + outPort.getName() + "] -> " + targetFilter.mName
+ + "[" + inPort.getName() + "]";
+ FrameQueue queue = builder.build(queueName);
+ outPort.setQueue(queue);
+ inPort.setQueue(queue);
+ } catch (RuntimeException e) {
+ throw new RuntimeException("Could not open output port " + outPort + "!", e);
+ }
+ }
+ }
+
+ final boolean isSleeping() {
+ return mIsSleeping.get();
+ }
+
+ final long getLastScheduleTime() {
+ return mLastScheduleTime ;
+ }
+
+ private final void autoPullInputs() {
+ // [Non-iterator looping]
+ for (int i = 0; i < mConnectedInputPortArray.length; ++i) {
+ InputPort port = mConnectedInputPortArray[i];
+ if (port.hasFrame() && port.isAutoPullEnabled()) {
+ mConnectedInputPortArray[i].pullFrame();
+ }
+ }
+ }
+
+ private final void autoReleaseFrames() {
+ // [Non-iterator looping]
+ for (int i = 0; i < mAutoReleaseFrames.size(); ++i) {
+ mAutoReleaseFrames.get(i).release();
+ }
+ mAutoReleaseFrames.clear();
+ }
+
+ private final InputPort newInputPort(String name) {
+ InputPort result = mConnectedInputPorts.get(name);
+ if (result == null) {
+ Signature.PortInfo info = getSignature().getInputPortInfo(name);
+ result = new InputPort(this, name, info);
+ mConnectedInputPorts.put(name, result);
+ }
+ return result;
+ }
+
+ private final OutputPort newOutputPort(String name) {
+ OutputPort result = mConnectedOutputPorts.get(name);
+ if (result == null) {
+ Signature.PortInfo info = getSignature().getOutputPortInfo(name);
+ result = new OutputPort(this, name, info);
+ mConnectedOutputPorts.put(name, result);
+ }
+ return result;
+ }
+
+ private final void processRequests() {
+ if ((mRequests & REQUEST_FLAG_CLOSE) != 0) {
+ performClose();
+ mRequests = REQUEST_FLAG_NONE;
+ }
+ }
+
+ private void assertIsPaused() {
+ GraphRunner runner = GraphRunner.current();
+ if (runner != null && !runner.isPaused() && !runner.isStopped()) {
+ throw new RuntimeException("Attempting to modify filter state while runner is "
+ + "executing. Please pause or stop the runner first!");
+ }
+ }
+
+ private final void updatePortArrays() {
+ // Copy our port-maps to arrays for faster non-iterator access
+ mConnectedInputPortArray = mConnectedInputPorts.values().toArray(new InputPort[0]);
+ mConnectedOutputPortArray = mConnectedOutputPorts.values().toArray(new OutputPort[0]);
+ }
+
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterFactory.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterFactory.java
new file mode 100644
index 0000000000000..2c67c793bc0f2
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterFactory.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package androidx.media.filterfw;
+
+import android.util.Log;
+
+import dalvik.system.PathClassLoader;
+
+import java.lang.reflect.Constructor;
+import java.util.HashSet;
+
+public class FilterFactory {
+
+ private static FilterFactory mSharedFactory;
+ private HashSet mPackages = new HashSet();
+
+ private static ClassLoader mCurrentClassLoader;
+ private static HashSet mLibraries;
+ private static Object mClassLoaderGuard;
+
+ static {
+ mCurrentClassLoader = Thread.currentThread().getContextClassLoader();
+ mLibraries = new HashSet();
+ mClassLoaderGuard = new Object();
+ }
+
+ private static final String TAG = "FilterFactory";
+ private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
+
+ public static FilterFactory sharedFactory() {
+ if (mSharedFactory == null) {
+ mSharedFactory = new FilterFactory();
+ }
+ return mSharedFactory;
+ }
+
+ /**
+ * Adds a new Java library to the list to be scanned for filters.
+ * libraryPath must be an absolute path of the jar file. This needs to be
+ * static because only one classloader per process can open a shared native
+ * library, which a filter may well have.
+ */
+ public static void addFilterLibrary(String libraryPath) {
+ if (mLogVerbose) Log.v(TAG, "Adding filter library " + libraryPath);
+ synchronized(mClassLoaderGuard) {
+ if (mLibraries.contains(libraryPath)) {
+ if (mLogVerbose) Log.v(TAG, "Library already added");
+ return;
+ }
+ mLibraries.add(libraryPath);
+ // Chain another path loader to the current chain
+ mCurrentClassLoader = new PathClassLoader(libraryPath, mCurrentClassLoader);
+ }
+ }
+
+ public void addPackage(String packageName) {
+ if (mLogVerbose) Log.v(TAG, "Adding package " + packageName);
+ /* TODO: This should use a getPackage call in the caller's context, but no such method
+ exists.
+ Package pkg = Package.getPackage(packageName);
+ if (pkg == null) {
+ throw new IllegalArgumentException("Unknown filter package '" + packageName + "'!");
+ }
+ */
+ mPackages.add(packageName);
+ }
+
+ public boolean isFilterAvailable(String className) {
+ return getFilterClass(className) != null;
+ }
+
+ public Filter createFilterByClassName(String className, String filterName, MffContext context) {
+ if (mLogVerbose) Log.v(TAG, "Looking up class " + className);
+ Class extends Filter> filterClass = getFilterClass(className);
+ if (filterClass == null) {
+ throw new IllegalArgumentException("Unknown filter class '" + className + "'!");
+ }
+ return createFilterByClass(filterClass, filterName, context);
+ }
+
+ public Filter createFilterByClass(Class extends Filter> filterClass,
+ String filterName, MffContext context) {
+ // Look for the correct constructor
+ Constructor extends Filter> filterConstructor = null;
+ try {
+ filterConstructor = filterClass.getConstructor(MffContext.class, String.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("The filter class '" + filterClass
+ + "' does not have a constructor of the form (MffContext, String)!");
+ }
+
+ // Construct the filter
+ Filter filter = null;
+ try {
+ filter = filterConstructor.newInstance(context, filterName);
+ } catch (Throwable t) {
+ throw new RuntimeException("Error creating filter " + filterName + "!", t);
+ }
+
+ if (filter == null) {
+ throw new IllegalArgumentException("Could not construct the filter '"
+ + filterName + "'!");
+ }
+ return filter;
+ }
+
+ private Class extends Filter> getFilterClass(String name) {
+ Class> filterClass = null;
+
+ // Look for the class in the imported packages
+ for (String packageName : mPackages) {
+ try {
+ if (mLogVerbose) Log.v(TAG, "Trying "+ packageName + "." + name);
+ synchronized(mClassLoaderGuard) {
+ filterClass = mCurrentClassLoader.loadClass(packageName + "." + name);
+ }
+ } catch (ClassNotFoundException e) {
+ continue;
+ }
+ // Exit loop if class was found.
+ if (filterClass != null) {
+ break;
+ }
+ }
+ Class extends Filter> result = null;
+ try {
+ if (filterClass != null) {
+ result = filterClass.asSubclass(Filter.class);
+ }
+ } catch (ClassCastException e) {
+ // Leave result == null
+ }
+ return result;
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterGraph.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterGraph.java
new file mode 100644
index 0000000000000..7d5ed9f438402
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterGraph.java
@@ -0,0 +1,567 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.util.Log;
+import android.view.View;
+import androidx.media.filterpacks.base.BranchFilter;
+import androidx.media.filterpacks.base.FrameSlotSource;
+import androidx.media.filterpacks.base.FrameSlotTarget;
+import androidx.media.filterpacks.base.GraphInputSource;
+import androidx.media.filterpacks.base.GraphOutputTarget;
+import androidx.media.filterpacks.base.ValueTarget;
+import androidx.media.filterpacks.base.ValueTarget.ValueListener;
+import androidx.media.filterpacks.base.VariableSource;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * A graph of Filter nodes.
+ *
+ * A FilterGraph instance contains a set of Filter instances connected by their output and input
+ * ports. Every filter belongs to exactly one graph and cannot be moved to another graph.
+ *
+ * FilterGraphs may contain sub-graphs that are dependent on the parent graph. These are typically
+ * used when inserting sub-graphs into MetaFilters. When a parent graph is torn down so are its
+ * sub-graphs. The same applies to flushing frames of a graph.
+ */
+public class FilterGraph {
+
+ private final static boolean DEBUG = false;
+
+ /** The context that this graph lives in */
+ private MffContext mContext;
+
+ /** Map from name of filter to the filter instance */
+ private HashMap mFilterMap = new HashMap();
+
+ /** Allows quick access to array of all filters. */
+ private Filter[] mAllFilters = null;
+
+ /** The GraphRunner currently attached to this graph */
+ GraphRunner mRunner;
+
+ /** The set of sub-graphs of this graph */
+ HashSet mSubGraphs = new HashSet();
+
+ /** The parent graph of this graph, or null it this graph is a root graph. */
+ private FilterGraph mParentGraph;
+
+ public static class Builder {
+
+ /** The context that this builder lives in */
+ private MffContext mContext;
+
+ /** Map from name of filter to the filter instance */
+ private HashMap mFilterMap = new HashMap();
+
+ /**
+ * Creates a new builder for specifying a graph structure.
+ * @param context The context the graph will live in.
+ */
+ public Builder(MffContext context) {
+ mContext = context;
+ }
+
+ /**
+ * Add a filter to the graph.
+ *
+ * Adds the specified filter to the set of filters of this graph. The filter must not be in
+ * the graph already, and the filter's name must be unique within the graph.
+ *
+ * @param filter the filter to add to the graph.
+ * @throws IllegalArgumentException if the filter is in the graph already, or its name is
+ * is already taken.
+ */
+ public void addFilter(Filter filter) {
+ if (mFilterMap.values().contains(filter)) {
+ throw new IllegalArgumentException("Attempting to add filter " + filter + " that "
+ + "is in the graph already!");
+ } else if (mFilterMap.containsKey(filter.getName())) {
+ throw new IllegalArgumentException("Graph contains filter with name '"
+ + filter.getName() + "' already!");
+ } else {
+ mFilterMap.put(filter.getName(), filter);
+ }
+ }
+
+ /**
+ * Adds a variable to the graph.
+ *
+ * TODO: More documentation.
+ *
+ * @param name the name of the variable.
+ * @param value the value of the variable or null if no value is to be set yet.
+ * @return the VariableSource filter that holds the value of this variable.
+ */
+ public VariableSource addVariable(String name, Object value) {
+ if (getFilter(name) != null) {
+ throw new IllegalArgumentException("Filter named '" + name + "' exists already!");
+ }
+ VariableSource valueSource = new VariableSource(mContext, name);
+ addFilter(valueSource);
+ if (value != null) {
+ valueSource.setValue(value);
+ }
+ return valueSource;
+ }
+
+ public FrameSlotSource addFrameSlotSource(String name, String slotName) {
+ FrameSlotSource filter = new FrameSlotSource(mContext, name, slotName);
+ addFilter(filter);
+ return filter;
+ }
+
+ public FrameSlotTarget addFrameSlotTarget(String name, String slotName) {
+ FrameSlotTarget filter = new FrameSlotTarget(mContext, name, slotName);
+ addFilter(filter);
+ return filter;
+ }
+
+ /**
+ * Connect two filters by their ports.
+ * The filters specified must have been previously added to the graph builder.
+ *
+ * @param sourceFilterName The name of the source filter.
+ * @param sourcePort The name of the source port.
+ * @param targetFilterName The name of the target filter.
+ * @param targetPort The name of the target port.
+ */
+ public void connect(String sourceFilterName, String sourcePort,
+ String targetFilterName, String targetPort) {
+ Filter sourceFilter = getFilter(sourceFilterName);
+ Filter targetFilter = getFilter(targetFilterName);
+ if (sourceFilter == null) {
+ throw new IllegalArgumentException("Unknown filter '" + sourceFilterName + "'!");
+ } else if (targetFilter == null) {
+ throw new IllegalArgumentException("Unknown filter '" + targetFilterName + "'!");
+ }
+ connect(sourceFilter, sourcePort, targetFilter, targetPort);
+ }
+
+ /**
+ * Connect two filters by their ports.
+ * The filters specified must have been previously added to the graph builder.
+ *
+ * @param sourceFilter The source filter.
+ * @param sourcePort The name of the source port.
+ * @param targetFilter The target filter.
+ * @param targetPort The name of the target port.
+ */
+ public void connect(Filter sourceFilter, String sourcePort,
+ Filter targetFilter, String targetPort) {
+ sourceFilter.connect(sourcePort, targetFilter, targetPort);
+ }
+
+ /**
+ * Returns the filter with the specified name.
+ *
+ * @return the filter with the specified name, or null if no such filter exists.
+ */
+ public Filter getFilter(String name) {
+ return mFilterMap.get(name);
+ }
+
+ /**
+ * Builds the graph and checks signatures.
+ *
+ * @return The new graph instance.
+ */
+ public FilterGraph build() {
+ checkSignatures();
+ return buildWithParent(null);
+ }
+
+ /**
+ * Builds the sub-graph and checks signatures.
+ *
+ * @param parentGraph the parent graph of the built sub-graph.
+ * @return The new graph instance.
+ */
+ public FilterGraph buildSubGraph(FilterGraph parentGraph) {
+ if (parentGraph == null) {
+ throw new NullPointerException("Parent graph must be non-null!");
+ }
+ checkSignatures();
+ return buildWithParent(parentGraph);
+ }
+
+ VariableSource assignValueToFilterInput(Object value, String filterName, String inputName) {
+ // Get filter to connect to
+ Filter filter = getFilter(filterName);
+ if (filter == null) {
+ throw new IllegalArgumentException("Unknown filter '" + filterName + "'!");
+ }
+
+ // Construct a name for our value source and make sure it does not exist already
+ String valueSourceName = filterName + "." + inputName;
+ if (getFilter(valueSourceName) != null) {
+ throw new IllegalArgumentException("VariableSource for '" + filterName + "' and "
+ + "input '" + inputName + "' exists already!");
+ }
+
+ // Create new VariableSource and connect it to the target filter and port
+ VariableSource valueSource = new VariableSource(mContext, valueSourceName);
+ addFilter(valueSource);
+ try {
+ ((Filter)valueSource).connect("value", filter, inputName);
+ } catch (RuntimeException e) {
+ throw new RuntimeException("Could not connect VariableSource to input '" + inputName
+ + "' of filter '" + filterName + "'!", e);
+ }
+
+ // Assign the value to the VariableSource
+ if (value != null) {
+ valueSource.setValue(value);
+ }
+
+ return valueSource;
+ }
+
+ VariableSource assignVariableToFilterInput(String varName,
+ String filterName,
+ String inputName) {
+ // Get filter to connect to
+ Filter filter = getFilter(filterName);
+ if (filter == null) {
+ throw new IllegalArgumentException("Unknown filter '" + filterName + "'!");
+ }
+
+ // Get variable
+ Filter variable = getFilter(varName);
+ if (variable == null || !(variable instanceof VariableSource)) {
+ throw new IllegalArgumentException("Unknown variable '" + varName + "'!");
+ }
+
+ // Connect variable (and possibly branch) variable to filter
+ try {
+ connectAndBranch(variable, "value", filter, inputName);
+ } catch (RuntimeException e) {
+ throw new RuntimeException("Could not connect VariableSource to input '" + inputName
+ + "' of filter '" + filterName + "'!", e);
+ }
+
+ return (VariableSource)variable;
+ }
+
+ /**
+ * Builds the graph without checking signatures.
+ * If parent is non-null, build a sub-graph of the specified parent.
+ *
+ * @return The new graph instance.
+ */
+ private FilterGraph buildWithParent(FilterGraph parent) {
+ FilterGraph graph = new FilterGraph(mContext, parent);
+ graph.mFilterMap = mFilterMap;
+ graph.mAllFilters = mFilterMap.values().toArray(new Filter[0]);
+ for (Entry filterEntry : mFilterMap.entrySet()) {
+ filterEntry.getValue().insertIntoFilterGraph(graph);
+ }
+ return graph;
+ }
+
+ private void checkSignatures() {
+ checkSignaturesForFilters(mFilterMap.values());
+ }
+
+ // TODO: Currently this always branches even if the connection is a 1:1 connection. Later
+ // we may optimize to pass through directly in the 1:1 case (may require disconnecting
+ // ports).
+ private void connectAndBranch(Filter sourceFilter,
+ String sourcePort,
+ Filter targetFilter,
+ String targetPort) {
+ String branchName = "__" + sourceFilter.getName() + "_" + sourcePort + "Branch";
+ Filter branch = getFilter(branchName);
+ if (branch == null) {
+ branch = new BranchFilter(mContext, branchName, false);
+ addFilter(branch);
+ sourceFilter.connect(sourcePort, branch, "input");
+ }
+ String portName = "to" + targetFilter.getName() + "_" + targetPort;
+ branch.connect(portName, targetFilter, targetPort);
+ }
+
+ }
+
+ /**
+ * Attach the graph and its subgraphs to a custom GraphRunner.
+ *
+ * Call this if you want the graph to be executed by a specific GraphRunner. You must call
+ * this before any other runner is set. Note that calls to {@code getRunner()} and
+ * {@code run()} auto-create a GraphRunner.
+ *
+ * @param runner The GraphRunner instance that should execute this graph.
+ * @see #getRunner()
+ * @see #run()
+ */
+ public void attachToRunner(GraphRunner runner) {
+ if (mRunner == null) {
+ for (FilterGraph subGraph : mSubGraphs) {
+ subGraph.attachToRunner(runner);
+ }
+ runner.attachGraph(this);
+ mRunner = runner;
+ } else if (mRunner != runner) {
+ throw new RuntimeException("Cannot attach FilterGraph to GraphRunner that is already "
+ + "attached to another GraphRunner!");
+ }
+ }
+
+ /**
+ * Forcibly tear down a filter graph.
+ *
+ * Call this to release any resources associated with the filter graph, its filters and any of
+ * its sub-graphs. This method must not be called if the graph (or any sub-graph) is running.
+ *
+ * You may no longer access this graph instance or any of its subgraphs after calling this
+ * method.
+ *
+ * Tearing down of sub-graphs is not supported. You must tear down the root graph, which will
+ * tear down all of its sub-graphs.
+ *
+ * @throws IllegalStateException if the graph is still running.
+ * @throws RuntimeException if you attempt to tear down a sub-graph.
+ */
+ public void tearDown() {
+ assertNotRunning();
+ if (mParentGraph != null) {
+ throw new RuntimeException("Attempting to tear down sub-graph!");
+ }
+ if (mRunner != null) {
+ mRunner.tearDownGraph(this);
+ }
+ for (FilterGraph subGraph : mSubGraphs) {
+ subGraph.mParentGraph = null;
+ subGraph.tearDown();
+ }
+ mSubGraphs.clear();
+ }
+
+ /**
+ * Returns the context of the graph.
+ *
+ * @return the MffContext instance that this graph is bound to.
+ */
+ public MffContext getContext() {
+ return mContext;
+ }
+
+ /**
+ * Returns the filter with the specified name.
+ *
+ * @return the filter with the specified name, or null if no such filter exists.
+ */
+ public Filter getFilter(String name) {
+ return mFilterMap.get(name);
+ }
+
+ /**
+ * Returns the VariableSource for the specified variable.
+ *
+ * TODO: More documentation.
+ * TODO: More specialized error handling.
+ *
+ * @param name The name of the VariableSource.
+ * @return The VariableSource filter instance with the specified name.
+ */
+ public VariableSource getVariable(String name) {
+ Filter result = mFilterMap.get(name);
+ if (result != null && result instanceof VariableSource) {
+ return (VariableSource)result;
+ } else {
+ throw new IllegalArgumentException("Unknown variable '" + name + "' specified!");
+ }
+ }
+
+ /**
+ * Returns the GraphOutputTarget with the specified name.
+ *
+ * @param name The name of the target.
+ * @return The GraphOutputTarget instance with the specified name.
+ */
+ public GraphOutputTarget getGraphOutput(String name) {
+ Filter result = mFilterMap.get(name);
+ if (result != null && result instanceof GraphOutputTarget) {
+ return (GraphOutputTarget)result;
+ } else {
+ throw new IllegalArgumentException("Unknown target '" + name + "' specified!");
+ }
+ }
+
+ /**
+ * Returns the GraphInputSource with the specified name.
+ *
+ * @param name The name of the source.
+ * @return The GraphInputSource instance with the specified name.
+ */
+ public GraphInputSource getGraphInput(String name) {
+ Filter result = mFilterMap.get(name);
+ if (result != null && result instanceof GraphInputSource) {
+ return (GraphInputSource)result;
+ } else {
+ throw new IllegalArgumentException("Unknown source '" + name + "' specified!");
+ }
+ }
+
+ /**
+ * Binds a filter to a view.
+ *
+ * ViewFilter instances support visualizing their data to a view. See the specific filter
+ * documentation for details. Views may be bound only if the graph is not running.
+ *
+ * @param filterName the name of the filter to bind.
+ * @param view the view to bind to.
+ * @throws IllegalStateException if the filter is in an illegal state.
+ * @throws IllegalArgumentException if no such view-filter exists.
+ */
+ public void bindFilterToView(String filterName, View view) {
+ Filter filter = mFilterMap.get(filterName);
+ if (filter != null && filter instanceof ViewFilter) {
+ ((ViewFilter)filter).bindToView(view);
+ } else {
+ throw new IllegalArgumentException("Unknown view filter '" + filterName + "'!");
+ }
+ }
+
+ /**
+ * TODO: Documentation.
+ */
+ public void bindValueTarget(String filterName, ValueListener listener, boolean onCallerThread) {
+ Filter filter = mFilterMap.get(filterName);
+ if (filter != null && filter instanceof ValueTarget) {
+ ((ValueTarget)filter).setListener(listener, onCallerThread);
+ } else {
+ throw new IllegalArgumentException("Unknown ValueTarget filter '" + filterName + "'!");
+ }
+ }
+
+ // Running Graphs //////////////////////////////////////////////////////////////////////////////
+ /**
+ * Convenience method to run the graph.
+ *
+ * Creates a new runner for this graph in the specified mode and executes it. Returns the
+ * runner to allow control of execution.
+ *
+ * @throws IllegalStateException if the graph is already running.
+ * @return the GraphRunner instance that was used for execution.
+ */
+ public GraphRunner run() {
+ GraphRunner runner = getRunner();
+ runner.setIsVerbose(false);
+ runner.start(this);
+ return runner;
+ }
+
+ /**
+ * Returns the GraphRunner for this graph.
+ *
+ * Every FilterGraph instance has a GraphRunner instance associated with it for executing the
+ * graph.
+ *
+ * @return the GraphRunner instance for this graph.
+ */
+ public GraphRunner getRunner() {
+ if (mRunner == null) {
+ GraphRunner runner = new GraphRunner(mContext);
+ attachToRunner(runner);
+ }
+ return mRunner;
+ }
+
+ /**
+ * Returns whether the graph is currently running.
+ *
+ * @return true if the graph is currently running.
+ */
+ public boolean isRunning() {
+ return mRunner != null && mRunner.isRunning();
+ }
+
+ /**
+ * Check each filter's signatures if all requirements are fulfilled.
+ *
+ * This will throw a RuntimeException if any unfulfilled requirements are found.
+ * Note that FilterGraph.Builder also has a function checkSignatures(), which allows
+ * to do the same /before/ the FilterGraph is built.
+ */
+ public void checkSignatures() {
+ checkSignaturesForFilters(mFilterMap.values());
+ }
+
+ // MFF Internal Methods ////////////////////////////////////////////////////////////////////////
+ Filter[] getAllFilters() {
+ return mAllFilters;
+ }
+
+ static void checkSignaturesForFilters(Collection filters) {
+ for (Filter filter : filters) {
+ if (DEBUG) {
+ Log.d("FilterGraph", "Checking filter " + filter.getName() + "...");
+ }
+ Signature signature = filter.getSignature();
+ signature.checkInputPortsConform(filter);
+ signature.checkOutputPortsConform(filter);
+ }
+ }
+
+ /**
+ * Wipes the filter references in this graph, so that they may be collected.
+ *
+ * This must be called only after a tearDown as this will make the FilterGraph invalid.
+ */
+ void wipe() {
+ mAllFilters = null;
+ mFilterMap = null;
+ }
+
+ void flushFrames() {
+ for (Filter filter : mFilterMap.values()) {
+ for (InputPort inputPort : filter.getConnectedInputPorts()) {
+ inputPort.clear();
+ }
+ for (OutputPort outputPort : filter.getConnectedOutputPorts()) {
+ outputPort.clear();
+ }
+ }
+ }
+
+ Set getSubGraphs() {
+ return mSubGraphs;
+ }
+
+ // Internal Methods ////////////////////////////////////////////////////////////////////////////
+ private FilterGraph(MffContext context, FilterGraph parentGraph) {
+ mContext = context;
+ mContext.addGraph(this);
+ if (parentGraph != null) {
+ mParentGraph = parentGraph;
+ mParentGraph.mSubGraphs.add(this);
+ }
+ }
+
+ private void assertNotRunning() {
+ if (isRunning()) {
+ throw new IllegalStateException("Attempting to modify running graph!");
+ }
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Frame.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Frame.java
new file mode 100644
index 0000000000000..67907d37c15ad
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Frame.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import java.util.Arrays;
+
+/**
+ * Frames are the data containers that are transported between Filters.
+ *
+ * Frames may be used only within a Filter during filter graph execution. Accessing Frames outside
+ * of graph execution may cause unexpected results.
+ *
+ * There are two ways to obtain new Frame instances. You can call
+ * {@link OutputPort#fetchAvailableFrame(int[])} on an OutputPort to obtain a Frame to pass to an
+ * output. You can also call {@link #create(FrameType, int[])} to obtain
+ * a detached Frame instance that you may hold onto in your filter. If you need to hold on to a
+ * Frame that is owned by an input or output queue, you must call
+ * {@link #retain()} on it.
+ *
+ * When you are done using a detached Frame, you must release it yourself.
+ *
+ * To access frame data, call any of the {@code lock}-methods. This will give you access to the
+ * frame data in the desired format. You must pass in a {@code mode} indicating whether you wish
+ * to read or write to the data. Writing to a read-locked Frame may produce unexpected results and
+ * interfere with other filters. When you are done reading or writing to the data, you must call
+ * {@link #unlock()}. Note, that a Frame must be unlocked before you push it into an output queue.
+ *
+ * Generally, any type of access format to a Frame's data will be granted. However, it is strongly
+ * recommended to specify the access format that you intend to use in your filter's signature or
+ * in the access flags passed to {@code newFrame()}. This will allow the Frame to allocate
+ * the most efficient backings for the intended type of access.
+ *
+ * A frame can be be pushed to an OutputPort by calling the {@link OutputPort#pushFrame(Frame)}
+ * method. Frames that have been pushed become read-only, and can no longer be modified.
+ *
+ * On the other end, a Filter can pull in an input Frame by calling {@link InputPort#pullFrame()}
+ * on the desired InputPort. Such frames are always read-only.
+ */
+public class Frame {
+
+ /** Special timestamp value indicating that no time-stamp was set. */
+ public static final long TIMESTAMP_NOT_SET = -1;
+
+ /** Frame data access mode: Read */
+ public static final int MODE_READ = 1;
+ /** Frame data access mode: Write */
+ public static final int MODE_WRITE = 2;
+
+ BackingStore mBackingStore;
+ boolean mReadOnly = false;
+
+ // Public API //////////////////////////////////////////////////////////////////////////////////
+ /**
+ * Returns the frame's type.
+ * @return A FrameType instance describing the frame data-type.
+ */
+ public final FrameType getType() {
+ return mBackingStore.getFrameType();
+ }
+
+ public final int getElementCount() {
+ return mBackingStore.getElementCount();
+ }
+
+ /**
+ * Set the frame's timestamp in nanoseconds.
+ *
+ * @param timestamp the timestamp of this frame in nanoseconds.
+ */
+ public final void setTimestamp(long timestamp) {
+ mBackingStore.setTimestamp(timestamp);
+ }
+
+ /**
+ * @return the frame's timestamp in nanoseconds.
+ */
+ public final long getTimestamp() {
+ return mBackingStore.getTimestamp();
+ }
+
+ /**
+ * @return the frame's timestamp in milliseconds.
+ */
+ public final long getTimestampMillis() {
+ return mBackingStore.getTimestamp() / 1000000L;
+ }
+
+ public final boolean isReadOnly() {
+ return mReadOnly;
+ }
+
+ public final FrameValue asFrameValue() {
+ return FrameValue.create(mBackingStore);
+ }
+
+ public final FrameValues asFrameValues() {
+ return FrameValues.create(mBackingStore);
+ }
+
+ public final FrameBuffer1D asFrameBuffer1D() {
+ return FrameBuffer1D.create(mBackingStore);
+ }
+
+ public final FrameBuffer2D asFrameBuffer2D() {
+ return FrameBuffer2D.create(mBackingStore);
+ }
+
+ public final FrameImage2D asFrameImage2D() {
+ return FrameImage2D.create(mBackingStore);
+ }
+
+ @Override
+ public String toString() {
+ return "Frame[" + getType().toString() + "]: " + mBackingStore;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return object instanceof Frame && ((Frame)object).mBackingStore == mBackingStore;
+ }
+
+ public static Frame create(FrameType type, int[] dimensions) {
+ FrameManager manager = FrameManager.current();
+ if (manager == null) {
+ throw new IllegalStateException("Attempting to create new Frame outside of "
+ + "FrameManager context!");
+ }
+ return new Frame(type, dimensions, manager);
+ }
+
+ public final Frame release() {
+ mBackingStore = mBackingStore.release();
+ return mBackingStore != null ? this : null;
+ }
+
+ public final Frame retain() {
+ mBackingStore = mBackingStore.retain();
+ return this;
+ }
+
+ public void unlock() {
+ if (!mBackingStore.unlock()) {
+ throw new RuntimeException("Attempting to unlock frame that is not locked!");
+ }
+ }
+
+ public int[] getDimensions() {
+ int[] dim = mBackingStore.getDimensions();
+ return dim != null ? Arrays.copyOf(dim, dim.length) : null;
+ }
+
+ Frame(FrameType type, int[] dimensions, FrameManager manager) {
+ mBackingStore = new BackingStore(type, dimensions, manager);
+ }
+
+ Frame(BackingStore backingStore) {
+ mBackingStore = backingStore;
+ }
+
+ final void assertAccessible(int mode) {
+ // Make sure frame is in write-mode
+ if (mReadOnly && mode == MODE_WRITE) {
+ throw new RuntimeException("Attempting to write to read-only frame " + this + "!");
+ }
+ }
+
+ final void setReadOnly(boolean readOnly) {
+ mReadOnly = readOnly;
+ }
+
+ void resize(int[] newDims) {
+ int[] oldDims = mBackingStore.getDimensions();
+ int oldCount = oldDims == null ? 0 : oldDims.length;
+ int newCount = newDims == null ? 0 : newDims.length;
+ if (oldCount != newCount) {
+ throw new IllegalArgumentException("Cannot resize " + oldCount + "-dimensional "
+ + "Frame to " + newCount + "-dimensional Frame!");
+ } else if (newDims != null && !Arrays.equals(oldDims, newDims)) {
+ mBackingStore.resize(newDims);
+ }
+ }
+
+ Frame makeCpuCopy(FrameManager frameManager) {
+ Frame frame = new Frame(getType(), getDimensions(), frameManager);
+ frame.mBackingStore.importStore(mBackingStore);
+ return frame;
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer1D.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer1D.java
new file mode 100644
index 0000000000000..0e24f5be954a8
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer1D.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.annotation.TargetApi;
+import android.renderscript.Allocation;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class FrameBuffer1D extends Frame {
+
+ private int mLength = 0;
+
+ /**
+ * Access frame's data using a {@link ByteBuffer}.
+ * This is a convenience method and is equivalent to calling {@code lockData} with an
+ * {@code accessFormat} of {@code ACCESS_BYTES}.
+ * When writing to the {@link ByteBuffer}, the byte order should be always set to
+ * {@link ByteOrder#nativeOrder()}.
+ *
+ * @return The byte buffer instance holding the Frame's data.
+ */
+ public ByteBuffer lockBytes(int mode) {
+ assertAccessible(mode);
+ return (ByteBuffer)mBackingStore.lockData(mode, BackingStore.ACCESS_BYTES);
+ }
+
+ /**
+ * Access frame's data using a RenderScript {@link Allocation}.
+ * This is a convenience method and is equivalent to calling {@code lockData} with an
+ * {@code accessFormat} of {@code ACCESS_ALLOCATION}.
+ *
+ * @return The Allocation instance holding the Frame's data.
+ */
+ @TargetApi(11)
+ public Allocation lockAllocation(int mode) {
+ assertAccessible(mode);
+ return (Allocation) mBackingStore.lockData(mode, BackingStore.ACCESS_ALLOCATION);
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ @Override
+ public int[] getDimensions() {
+ return super.getDimensions();
+ }
+
+ /**
+ * TODO: Documentation. Note that frame contents are invalidated.
+ */
+ @Override
+ public void resize(int[] newDimensions) {
+ super.resize(newDimensions);
+ }
+
+ static FrameBuffer1D create(BackingStore backingStore) {
+ assertCanCreate(backingStore);
+ return new FrameBuffer1D(backingStore);
+ }
+
+ FrameBuffer1D(BackingStore backingStore) {
+ super(backingStore);
+ updateLength(backingStore.getDimensions());
+ }
+
+ static void assertCanCreate(BackingStore backingStore) {
+ FrameType type = backingStore.getFrameType();
+ if (type.getElementSize() == 0) {
+ throw new RuntimeException("Cannot access Frame of type " + type + " as a FrameBuffer "
+ + "instance!");
+ }
+ int[] dims = backingStore.getDimensions();
+ if (dims == null || dims.length == 0) {
+ throw new RuntimeException("Cannot access Frame with no dimensions as a FrameBuffer "
+ + "instance!");
+ }
+ }
+
+ void updateLength(int[] dimensions) {
+ mLength = 1;
+ for (int dim : dimensions) {
+ mLength *= dim;
+ }
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer2D.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer2D.java
new file mode 100644
index 0000000000000..6a7f12a6d5a47
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer2D.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+public class FrameBuffer2D extends FrameBuffer1D {
+
+ public int getWidth() {
+ return mBackingStore.getDimensions()[0];
+ }
+
+ public int getHeight() {
+ return mBackingStore.getDimensions()[1];
+ }
+
+ static FrameBuffer2D create(BackingStore backingStore) {
+ assertCanCreate(backingStore);
+ return new FrameBuffer2D(backingStore);
+ }
+
+ FrameBuffer2D(BackingStore backingStore) {
+ super(backingStore);
+ }
+
+ static void assertCanCreate(BackingStore backingStore) {
+ FrameBuffer1D.assertCanCreate(backingStore);
+ int[] dimensions = backingStore.getDimensions();
+ int dimCount = dimensions != null ? dimensions.length : 0;
+ if (dimCount != 2) {
+ throw new RuntimeException("Cannot access " + dimCount + "-dimensional Frame as a "
+ + "FrameBuffer2D instance!");
+ }
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameImage2D.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameImage2D.java
new file mode 100644
index 0000000000000..bca94f77ba192
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameImage2D.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import androidx.media.filterfw.BackingStore.Backing;
+
+public class FrameImage2D extends FrameBuffer2D {
+
+ /**
+ * Access frame's data using a TextureSource.
+ * This is a convenience method and is equivalent to calling {@code lockData} with an
+ * {@code accessFormat} of {@code ACCESS_TEXTURE}.
+ *
+ * @return The TextureSource instance holding the Frame's data.
+ */
+ public TextureSource lockTextureSource() {
+ return (TextureSource)mBackingStore.lockData(MODE_READ, BackingStore.ACCESS_TEXTURE);
+ }
+
+ /**
+ * Access frame's data using a RenderTarget.
+ * This is a convenience method and is equivalent to calling {@code lockData} with an
+ * {@code accessFormat} of {@code ACCESS_RENDERTARGET}.
+ *
+ * @return The RenderTarget instance holding the Frame's data.
+ */
+ public RenderTarget lockRenderTarget() {
+ return (RenderTarget)mBackingStore.lockData(MODE_WRITE, BackingStore.ACCESS_RENDERTARGET);
+ }
+
+ /**
+ * Assigns the pixel data of the specified bitmap.
+ *
+ * The RGBA pixel data will be extracted from the bitmap and assigned to the frame data. Note,
+ * that the colors are premultiplied with the alpha channel. If you wish to have
+ * non-premultiplied colors, you must pass the Frame through an
+ * {@code UnpremultiplyAlphaFilter}.
+ *
+ * @param bitmap The bitmap pixels to assign.
+ */
+ public void setBitmap(Bitmap bitmap) {
+ bitmap = convertToFrameType(bitmap, mBackingStore.getFrameType());
+ validateBitmapSize(bitmap, mBackingStore.getDimensions());
+ Backing backing = mBackingStore.lockBacking(MODE_WRITE, BackingStore.ACCESS_BITMAP);
+ backing.setData(bitmap);
+ mBackingStore.unlock();
+ }
+
+ /**
+ * Returns the RGBA image contents as a Bitmap instance.
+ *
+ * @return a Bitmap instance holding the RGBA Frame image content.
+ */
+ public Bitmap toBitmap() {
+ Bitmap result = (Bitmap)mBackingStore.lockData(MODE_READ, BackingStore.ACCESS_BITMAP);
+ mBackingStore.unlock();
+ return result;
+ }
+
+ /**
+ * Copies the image data from one frame to another.
+ *
+ * The source and target rectangles must be given in normalized coordinates, where 0,0 is the
+ * top-left of the image and 1,1 is the bottom-right.
+ *
+ * If the target rectangle is smaller than the target frame, the pixel values outside of the
+ * target rectangle are undefined.
+ *
+ * This method must be called within a Filter during execution. It supports both GL-enabled
+ * and GL-disabled run contexts.
+ *
+ * @param target The target frame to copy to.
+ * @param sourceRect The source rectangle in normalized coordinates.
+ * @param targetRect The target rectangle in normalized coordinates.
+ */
+ public void copyToFrame(FrameImage2D target, RectF sourceRect, RectF targetRect) {
+ if (GraphRunner.current().isOpenGLSupported()) {
+ gpuImageCopy(this, target, sourceRect, targetRect);
+ } else {
+ cpuImageCopy(this, target, sourceRect, targetRect);
+ }
+ }
+
+ static FrameImage2D create(BackingStore backingStore) {
+ assertCanCreate(backingStore);
+ return new FrameImage2D(backingStore);
+ }
+
+ FrameImage2D(BackingStore backingStore) {
+ super(backingStore);
+ }
+
+ static void assertCanCreate(BackingStore backingStore) {
+ FrameBuffer2D.assertCanCreate(backingStore);
+ }
+
+ private static Bitmap convertToFrameType(Bitmap bitmap, FrameType type) {
+ Bitmap.Config config = bitmap.getConfig();
+ Bitmap result = bitmap;
+ switch(type.getElementId()) {
+ case FrameType.ELEMENT_RGBA8888:
+ if (config != Bitmap.Config.ARGB_8888) {
+ result = bitmap.copy(Bitmap.Config.ARGB_8888, false);
+ if (result == null) {
+ throw new RuntimeException("Could not convert bitmap to frame-type " +
+ "RGBA8888!");
+ }
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported frame type '" + type + "' for " +
+ "bitmap assignment!");
+ }
+ return result;
+ }
+
+ private void validateBitmapSize(Bitmap bitmap, int[] dimensions) {
+ if (bitmap.getWidth() != dimensions[0] || bitmap.getHeight() != dimensions[1]) {
+ throw new IllegalArgumentException("Cannot assign bitmap of size " + bitmap.getWidth()
+ + "x" + bitmap.getHeight() + " to frame of size " + dimensions[0] + "x"
+ + dimensions[1] + "!");
+ }
+ }
+
+ private static void gpuImageCopy(
+ FrameImage2D srcImage, FrameImage2D dstImage, RectF srcRect, RectF dstRect) {
+ ImageShader idShader = RenderTarget.currentTarget().getIdentityShader();
+ // We briefly modify the shader
+ // TODO: Implement a safer way to save and restore a shared shader.
+ idShader.setSourceRect(srcRect);
+ idShader.setTargetRect(dstRect);
+ idShader.process(srcImage, dstImage);
+ // And reset it as others may use it as well
+ idShader.setSourceRect(0f, 0f, 1f, 1f);
+ idShader.setTargetRect(0f, 0f, 1f, 1f);
+ }
+
+ private static void cpuImageCopy(
+ FrameImage2D srcImage, FrameImage2D dstImage, RectF srcRect, RectF dstRect) {
+ // Convert rectangles to integer rectangles in image dimensions
+ Rect srcIRect = new Rect((int) srcRect.left * srcImage.getWidth(),
+ (int) srcRect.top * srcImage.getHeight(),
+ (int) srcRect.right * srcImage.getWidth(),
+ (int) srcRect.bottom * srcImage.getHeight());
+ Rect dstIRect = new Rect((int) dstRect.left * srcImage.getWidth(),
+ (int) dstRect.top * srcImage.getHeight(),
+ (int) dstRect.right * srcImage.getWidth(),
+ (int) dstRect.bottom * srcImage.getHeight());
+
+ // Create target canvas
+ Bitmap.Config config = Bitmap.Config.ARGB_8888;
+ Bitmap dstBitmap = Bitmap.createBitmap(dstImage.getWidth(), dstImage.getHeight(), config);
+ Canvas canvas = new Canvas(dstBitmap);
+
+ // Draw source bitmap into target canvas
+ Paint paint = new Paint();
+ paint.setFilterBitmap(true);
+ Bitmap srcBitmap = srcImage.toBitmap();
+ canvas.drawBitmap(srcBitmap, srcIRect, dstIRect, paint);
+
+ // Assign bitmap to output frame
+ dstImage.setBitmap(dstBitmap);
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameManager.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameManager.java
new file mode 100644
index 0000000000000..55ed277a349ee
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameManager.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import androidx.media.filterfw.BackingStore.Backing;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Set;
+
+/**
+ * The FrameManager tracks, caches, allocates and deallocates frame data.
+ * All Frame instances are managed by a FrameManager, and belong to exactly one of these. Frames
+ * cannot be shared across FrameManager instances, however multiple MffContexts may use the same
+ * FrameManager.
+ *
+ * Additionally, frame managers allow attaching Frames under a specified key. This allows decoupling
+ * filter-graphs by instructing one node to attach a frame under a specific key, and another to
+ * fetch the frame under the same key.
+ */
+public class FrameManager {
+
+ /** The default max cache size is set to 12 MB */
+ public final static int DEFAULT_MAX_CACHE_SIZE = 12 * 1024 * 1024;
+
+ /** Frame caching policy: No caching */
+ public final static int FRAME_CACHE_NONE = 0;
+ /** Frame caching policy: Drop least recently used frame buffers */
+ public final static int FRAME_CACHE_LRU = 1;
+ /** Frame caching policy: Drop least frequently used frame buffers */
+ public final static int FRAME_CACHE_LFU = 2;
+
+ /** Slot Flag: No flags set */
+ public final static int SLOT_FLAGS_NONE = 0x00;
+ /** Slot Flag: Sticky flag set: Frame will remain in slot after fetch. */
+ public final static int SLOT_FLAG_STICKY = 0x01;
+
+ private GraphRunner mRunner;
+ private Set mBackings = new HashSet();
+ private BackingCache mCache;
+
+ private Map mFrameSlots = new HashMap();
+
+ static class FrameSlot {
+ private FrameType mType;
+ private int mFlags;
+ private Frame mFrame = null;
+
+ public FrameSlot(FrameType type, int flags) {
+ mType = type;
+ mFlags = flags;
+ }
+
+ public FrameType getType() {
+ return mType;
+ }
+
+ public boolean hasFrame() {
+ return mFrame != null;
+ }
+
+ public void releaseFrame() {
+ if (mFrame != null) {
+ mFrame.release();
+ mFrame = null;
+ }
+ }
+
+ // TODO: Type check
+ public void assignFrame(Frame frame) {
+ Frame oldFrame = mFrame;
+ mFrame = frame.retain();
+ if (oldFrame != null) {
+ oldFrame.release();
+ }
+ }
+
+ public Frame getFrame() {
+ Frame result = mFrame.retain();
+ if ((mFlags & SLOT_FLAG_STICKY) == 0) {
+ releaseFrame();
+ }
+ return result;
+ }
+
+ public void markWritable() {
+ if (mFrame != null) {
+ mFrame.setReadOnly(false);
+ }
+ }
+ }
+
+ private static abstract class BackingCache {
+
+ protected int mCacheMaxSize = DEFAULT_MAX_CACHE_SIZE;
+
+ public abstract Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize);
+
+ public abstract boolean cacheBacking(Backing backing);
+
+ public abstract void clear();
+
+ public abstract int getSizeLeft();
+
+ public void setSize(int size) {
+ mCacheMaxSize = size;
+ }
+
+ public int getSize() {
+ return mCacheMaxSize;
+ }
+ }
+
+ private static class BackingCacheNone extends BackingCache {
+
+ @Override
+ public Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) {
+ return null;
+ }
+
+ @Override
+ public boolean cacheBacking(Backing backing) {
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ }
+
+ @Override
+ public int getSize() {
+ return 0;
+ }
+
+ @Override
+ public int getSizeLeft() {
+ return 0;
+ }
+ }
+
+ private static abstract class PriorityBackingCache extends BackingCache {
+ private int mSize = 0;
+ private PriorityQueue mQueue;
+
+ public PriorityBackingCache() {
+ mQueue = new PriorityQueue(4, new Comparator() {
+ @Override
+ public int compare(Backing left, Backing right) {
+ return left.cachePriority - right.cachePriority;
+ }
+ });
+ }
+
+ @Override
+ public Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) {
+ for (Backing backing : mQueue) {
+ int backingAccess = (mode == Frame.MODE_WRITE)
+ ? backing.writeAccess()
+ : backing.readAccess();
+ if ((backingAccess & access) == access
+ && dimensionsCompatible(backing.getDimensions(), dimensions)
+ && (elemSize == backing.getElementSize())) {
+ mQueue.remove(backing);
+ mSize -= backing.getSize();
+ onFetchBacking(backing);
+ return backing;
+ }
+ }
+ //Log.w("FrameManager", "Could not find backing for dimensions " + Arrays.toString(dimensions));
+ return null;
+ }
+
+ @Override
+ public boolean cacheBacking(Backing backing) {
+ if (reserve(backing.getSize())) {
+ onCacheBacking(backing);
+ mQueue.add(backing);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ mQueue.clear();
+ mSize = 0;
+ }
+
+ @Override
+ public int getSizeLeft() {
+ return mCacheMaxSize - mSize;
+ }
+
+ protected abstract void onCacheBacking(Backing backing);
+
+ protected abstract void onFetchBacking(Backing backing);
+
+ private boolean reserve(int size) {
+ //Log.i("FM", "Reserving " + size + " bytes (max: " + mCacheMaxSize + " bytes).");
+ //Log.i("FM", "Current size " + mSize);
+ if (size > mCacheMaxSize) {
+ return false;
+ }
+ mSize += size;
+ while (mSize > mCacheMaxSize) {
+ Backing dropped = mQueue.poll();
+ mSize -= dropped.getSize();
+ //Log.i("FM", "Dropping " + dropped + " with priority "
+ // + dropped.cachePriority + ". New size: " + mSize + "!");
+ dropped.destroy();
+ }
+ return true;
+ }
+
+
+ }
+
+ private static class BackingCacheLru extends PriorityBackingCache {
+ private int mTimestamp = 0;
+
+ @Override
+ protected void onCacheBacking(Backing backing) {
+ backing.cachePriority = 0;
+ }
+
+ @Override
+ protected void onFetchBacking(Backing backing) {
+ ++mTimestamp;
+ backing.cachePriority = mTimestamp;
+ }
+ }
+
+ private static class BackingCacheLfu extends PriorityBackingCache {
+ @Override
+ protected void onCacheBacking(Backing backing) {
+ backing.cachePriority = 0;
+ }
+
+ @Override
+ protected void onFetchBacking(Backing backing) {
+ ++backing.cachePriority;
+ }
+ }
+
+ public static FrameManager current() {
+ GraphRunner runner = GraphRunner.current();
+ return runner != null ? runner.getFrameManager() : null;
+ }
+
+ /**
+ * Returns the context that the FrameManager is bound to.
+ *
+ * @return the MffContext instance that the FrameManager is bound to.
+ */
+ public MffContext getContext() {
+ return mRunner.getContext();
+ }
+
+ /**
+ * Returns the GraphRunner that the FrameManager is bound to.
+ *
+ * @return the GraphRunner instance that the FrameManager is bound to.
+ */
+ public GraphRunner getRunner() {
+ return mRunner;
+ }
+
+ /**
+ * Sets the size of the cache.
+ *
+ * Resizes the cache to the specified size in bytes.
+ *
+ * @param bytes the new size in bytes.
+ */
+ public void setCacheSize(int bytes) {
+ mCache.setSize(bytes);
+ }
+
+ /**
+ * Returns the size of the cache.
+ *
+ * @return the size of the cache in bytes.
+ */
+ public int getCacheSize() {
+ return mCache.getSize();
+ }
+
+ /**
+ * Imports a frame from another FrameManager.
+ *
+ * This will return a frame with the contents of the given frame for use in this FrameManager.
+ * Note, that there is a substantial cost involved in moving a Frame from one FrameManager to
+ * another. This may be called from any thread. After the frame has been imported, it may be
+ * used in the runner that uses this FrameManager. As the new frame may share data with the
+ * provided frame, that frame must be read-only.
+ *
+ * @param frame The frame to import
+ */
+ public Frame importFrame(Frame frame) {
+ if (!frame.isReadOnly()) {
+ throw new IllegalArgumentException("Frame " + frame + " must be read-only to import "
+ + "into another FrameManager!");
+ }
+ return frame.makeCpuCopy(this);
+ }
+
+ /**
+ * Adds a new frame slot to the frame manager.
+ * Filters can reference frame slots to pass frames between graphs or runs. If the name
+ * specified here is already taken the frame slot is overwritten. You can only
+ * modify frame-slots while no graph of the frame manager is running.
+ *
+ * @param name The name of the slot.
+ * @param type The type of Frame that will be assigned to this slot.
+ * @param flags A mask of {@code SLOT} flags.
+ */
+ public void addFrameSlot(String name, FrameType type, int flags) {
+ assertNotRunning();
+ FrameSlot oldSlot = mFrameSlots.get(name);
+ if (oldSlot != null) {
+ removeFrameSlot(name);
+ }
+ FrameSlot slot = new FrameSlot(type, flags);
+ mFrameSlots.put(name, slot);
+ }
+
+ /**
+ * Removes a frame slot from the frame manager.
+ * Any frame within the slot is released. You can only modify frame-slots while no graph
+ * of the frame manager is running.
+ *
+ * @param name The name of the slot
+ * @throws IllegalArgumentException if no such slot exists.
+ */
+ public void removeFrameSlot(String name) {
+ assertNotRunning();
+ FrameSlot slot = getSlot(name);
+ slot.releaseFrame();
+ mFrameSlots.remove(slot);
+ }
+
+ /**
+ * TODO: Document!
+ */
+ public void storeFrame(Frame frame, String slotName) {
+ assertInGraphRun();
+ getSlot(slotName).assignFrame(frame);
+ }
+
+ /**
+ * TODO: Document!
+ */
+ public Frame fetchFrame(String slotName) {
+ assertInGraphRun();
+ return getSlot(slotName).getFrame();
+ }
+
+ /**
+ * Clears the Frame cache.
+ */
+ public void clearCache() {
+ mCache.clear();
+ }
+
+ /**
+ * Create a new FrameManager instance.
+ *
+ * Creates a new FrameManager instance in the specified context and employing a cache with the
+ * specified cache type (see the cache type constants defined by the FrameManager class).
+ *
+ * @param runner the GraphRunner to bind the FrameManager to.
+ * @param cacheType the type of cache to use.
+ */
+ FrameManager(GraphRunner runner, int cacheType) {
+ mRunner = runner;
+ switch (cacheType) {
+ case FRAME_CACHE_NONE:
+ mCache = new BackingCacheNone();
+ break;
+ case FRAME_CACHE_LRU:
+ mCache = new BackingCacheLru();
+ break;
+ case FRAME_CACHE_LFU:
+ mCache = new BackingCacheLfu();
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown cache-type " + cacheType + "!");
+ }
+ }
+
+ Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) {
+ return mCache.fetchBacking(mode, access, dimensions, elemSize);
+ }
+
+ void onBackingCreated(Backing backing) {
+ if (backing != null) {
+ mBackings.add(backing);
+ // Log.i("FrameManager", "RM: Now have " + mBackings.size() + " backings");
+ }
+ }
+
+ void onBackingAvailable(Backing backing) {
+ if (!backing.shouldCache() || !mCache.cacheBacking(backing)) {
+ backing.destroy();
+ mBackings.remove(backing);
+ //Log.i("FrameManager", "RM: Now have " + mBackings.size() + " backings (" + mCache.getSizeLeft() + ")");
+ }
+ }
+
+ /**
+ * Destroying all references makes any Frames that contain them invalid.
+ */
+ void destroyBackings() {
+ for (Backing backing : mBackings) {
+ backing.destroy();
+ }
+ mBackings.clear();
+ mCache.clear();
+ }
+
+ FrameSlot getSlot(String name) {
+ FrameSlot slot = mFrameSlots.get(name);
+ if (slot == null) {
+ throw new IllegalArgumentException("Unknown frame slot '" + name + "'!");
+ }
+ return slot;
+ }
+
+ void onBeginRun() {
+ for (FrameSlot slot : mFrameSlots.values()) {
+ slot.markWritable();
+ }
+ }
+
+ // Internals ///////////////////////////////////////////////////////////////////////////////////
+ private static boolean dimensionsCompatible(int[] dimA, int[] dimB) {
+ return dimA == null || dimB == null || Arrays.equals(dimA, dimB);
+ }
+
+ private void assertNotRunning() {
+ if (mRunner.isRunning()) {
+ throw new IllegalStateException("Attempting to modify FrameManager while graph is "
+ + "running!");
+ }
+ }
+
+ private void assertInGraphRun() {
+ if (!mRunner.isRunning() || GraphRunner.current() != mRunner) {
+ throw new IllegalStateException("Attempting to access FrameManager Frame data "
+ + "outside of graph run-loop!");
+ }
+ }
+
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameQueue.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameQueue.java
new file mode 100644
index 0000000000000..c26f9375b77c6
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameQueue.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package androidx.media.filterfw;
+
+import java.util.Vector;
+
+class FrameQueue {
+
+ public static class Builder {
+
+ private FrameType mReadType = null;
+ private FrameType mWriteType = null;
+
+ private Vector mAttachedQueues = new Vector();
+
+ public Builder() {}
+
+ public void setWriteType(FrameType type) {
+ mWriteType = type;
+ }
+
+ public void setReadType(FrameType type) {
+ mReadType = type;
+ }
+
+ public void attachQueue(FrameQueue queue) {
+ mAttachedQueues.add(queue);
+ }
+
+ public FrameQueue build(String name) {
+ FrameType type = buildType();
+ // TODO: This currently does not work correctly (Try camera -> branch -> target-slot)
+ //validateType(type, name);
+ FrameQueue result = new FrameQueue(type, name);
+ buildQueueImpl(result);
+ return result;
+ }
+
+ private void buildQueueImpl(FrameQueue queue) {
+ QueueImpl queueImpl = queue.new SingleFrameQueueImpl();
+ queue.mQueueImpl = queueImpl;
+ }
+
+ private FrameType buildType() {
+ FrameType result = FrameType.merge(mWriteType, mReadType);
+ for (FrameQueue queue : mAttachedQueues) {
+ result = FrameType.merge(result, queue.mType);
+ }
+ return result;
+ }
+
+ /*
+ private void validateType(FrameType type, String queueName) {
+ if (!type.isSpecified()) {
+ throw new RuntimeException("Cannot build connection queue '" + queueName + "' as "
+ + "its type (" + type + ") is underspecified!");
+ }
+ }
+ */
+ }
+
+ private interface QueueImpl {
+ public boolean canPull();
+
+ public boolean canPush();
+
+ public Frame pullFrame();
+
+ public Frame fetchAvailableFrame(int[] dimensions);
+
+ public Frame peek();
+
+ public void pushFrame(Frame frame);
+
+ public void clear();
+ }
+
+ private class SingleFrameQueueImpl implements QueueImpl {
+ private Frame mFrame = null;
+
+ @Override
+ public boolean canPull() {
+ return mFrame != null;
+ }
+
+ @Override
+ public boolean canPush() {
+ return mFrame == null;
+ }
+
+ @Override
+ public Frame pullFrame() {
+ Frame result = mFrame;
+ mFrame = null;
+ return result;
+ }
+
+ @Override
+ public Frame peek() {
+ return mFrame;
+ }
+
+ @Override
+ public Frame fetchAvailableFrame(int[] dimensions) {
+ // Note that we cannot use a cached frame here, as we do not know where that cached
+ // instance would end up.
+ FrameManager manager = FrameManager.current();
+ return new Frame(mType, dimensions, manager);
+ }
+
+ @Override
+ public void pushFrame(Frame frame) {
+ mFrame = frame.retain();
+ mFrame.setReadOnly(true);
+ }
+
+ @Override
+ public void clear() {
+ if (mFrame != null) {
+ mFrame.release();
+ mFrame = null;
+ }
+ }
+ }
+
+ private QueueImpl mQueueImpl;
+ private FrameType mType;
+ private String mName;
+
+ public FrameType getType() {
+ return mType;
+ }
+
+ public boolean canPull() {
+ return mQueueImpl.canPull();
+ }
+
+ public boolean canPush() {
+ return mQueueImpl.canPush();
+ }
+
+ public Frame pullFrame() {
+ return mQueueImpl.pullFrame();
+ }
+
+ public Frame fetchAvailableFrame(int[] dimensions) {
+ return mQueueImpl.fetchAvailableFrame(dimensions);
+ }
+
+ public void pushFrame(Frame frame) {
+ mQueueImpl.pushFrame(frame);
+ }
+
+ public Frame peek() {
+ return mQueueImpl.peek();
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+
+ public void clear() {
+ mQueueImpl.clear();
+ }
+
+ private FrameQueue(FrameType type, String name) {
+ mType = type;
+ mName = name;
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotSource.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotSource.java
new file mode 100644
index 0000000000000..0a093f93f60a7
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotSource.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterpacks.base;
+
+import androidx.media.filterfw.*;
+
+public final class FrameSlotSource extends SlotFilter {
+
+ public FrameSlotSource(MffContext context, String name, String slotName) {
+ super(context, name, slotName);
+ }
+
+ @Override
+ public Signature getSignature() {
+ // TODO: It would be nice if we could return the slot type here. Not currently possible
+ // as getSignature() is typically called before a FrameManager and its slots are setup.
+ return new Signature()
+ .addOutputPort("frame", Signature.PORT_REQUIRED, FrameType.any())
+ .disallowOtherPorts();
+ }
+
+ @Override
+ protected boolean canSchedule() {
+ return super.canSchedule() && slotHasFrame();
+ }
+
+ @Override
+ protected void onProcess() {
+ Frame frame = getFrameManager().fetchFrame(mSlotName);
+ getConnectedOutputPort("frame").pushFrame(frame);
+ frame.release();
+ }
+
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotTarget.java
new file mode 100644
index 0000000000000..55648c68ab324
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotTarget.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterpacks.base;
+
+import androidx.media.filterfw.*;
+
+public final class FrameSlotTarget extends SlotFilter {
+
+ public FrameSlotTarget(MffContext context, String name, String slotName) {
+ super(context, name, slotName);
+ }
+
+ @Override
+ public Signature getSignature() {
+ // TODO: It would be nice if we could return the slot type here. Not currently possible
+ // as getSignature() is typically called before a FrameManager and its slots are setup.
+ return new Signature()
+ .addInputPort("frame", Signature.PORT_REQUIRED, FrameType.any())
+ .disallowOtherPorts();
+ }
+
+ @Override
+ protected void onProcess() {
+ Frame frame = getConnectedInputPort("frame").pullFrame();
+ getFrameManager().storeFrame(frame, mSlotName);
+ }
+
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameType.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameType.java
new file mode 100644
index 0000000000000..bfa4018201a6c
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameType.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package androidx.media.filterfw;
+
+
+/**
+ * A FrameType instance specifies the data format of a Frame.
+ *
+ * FrameTypes are used mainly by Filters to specify the data type they intend to consume or produce.
+ * When filters are connected, their FrameType information is analyzed and checked for
+ * compatibility. This allows Filter writers to assume a certain data input type. It also helps
+ * filter-graph designers determine which filters can be hooked up to one another.
+ *
+ * A FrameType generally consists of an element type and number of dimensions. The currently
+ * supported element types are:
+ *
+ *
+ *
int8, int16, int32, in64
+ *
float32, float64
+ *
rgba8888
+ *
object
+ *
don't-care
+ *
+ *
+ * If the object element type is used, class information may be appended to the FrameType to
+ * indicate what class of objects are expected. When constructing an object based FrameType, you
+ * have the option of either specifying a type that represents a single object of that class, or
+ * an array of objects (see the {@link #single()} and {@link #array()} constructors). A single
+ * object has a dimensionality of 0, while an array has a dimensionality of 1.
+ *
+ * When constructing a non-object type, you have the option of creating a 1D or 2D buffer, or
+ * a 2D image (see the {@link #buffer1D(int)}, {@link #buffer2D(int)}, and
+ * {@link #image2D(int, int)} constructors). To optimize access, provide access hints when making
+ * an image type.
+ *
+ * Finally, it is possible to create a wild-card type with the {@link #any()} constructor. This
+ * type matches any other type. Note, that this is a more general type than a {@code single(Object)}
+ * type that matches only object-base types (of any Object subclass). You may also specify the
+ * leave the element of any type unspecified by using the {@code ELEMENT_DONTCARE} constant.
+ *
+ * When a graph is connected the types between outputs and inputs are merged to a queue-type. All
+ * Frames in this queue will be of that type. In order for a merge to succeed the following
+ * conditions must hold:
+ *
+ *
+ *
The element types must be identical.
+ *
The dimensions must match (except for singles and arrays, see below).
+ *
For object-based types: The classes must be compatible.
+ *
If one of the types is a wild-card, both types are always compatible.
+ *
+ *
+ * Class compatibility is determined in an optimistic fashion, i.e. one class must be the subclass
+ * of the other. It does not matter which of the types is the subclass of the other. For instance,
+ * if one Filter outputs a type of class {@code Object}, and the consumer expects a Filter of type
+ * {@code Bitmap}, the connection is considered compatible. (Of course if at runtime a non-Bitmap
+ * object is produced, this will cause a runtime exception to be thrown).
+ *
+ * For convenience, single and array object-based types are compatible with one another. This
+ * in turn means that Frames with a single object can be accessed as an array with a single entry,
+ * and array based Frames can be accessed as a single object of the array class. For this reason
+ * you should prefer consuming objects as array types (if it makes sense for that specific port),
+ * as this will allow your Filter to handle multiple objects in one Frame while not giving up the
+ * possibility to deal with singles.
+ * TODO: This needs to be reworked. An array(int) should not be interchangeable with a single(int),
+ * but rather with a single(int[]). Use ArraySelectFilter for the former!
+ *
+ * After the types are merged, the queue-type must be a fully specified type. This means that the
+ * type must have its element and dimensions specified. This ensures that filters that need to
+ * query their input or output types receive meaningful information.
+ */
+public final class FrameType {
+
+ public final static int ELEMENT_DONTCARE = 0;
+ public final static int ELEMENT_OBJECT = 1;
+
+ public final static int ELEMENT_INT8 = 100;
+ public final static int ELEMENT_INT16 = 101;
+ public final static int ELEMENT_INT32 = 102;
+ public final static int ELEMENT_INT64 = 103;
+
+ public final static int ELEMENT_FLOAT32 = 200;
+ public final static int ELEMENT_FLOAT64 = 201;
+
+ public final static int ELEMENT_RGBA8888 = 301;
+
+ public final static int READ_CPU = 0x01;
+ public final static int READ_GPU = 0x02;
+ public final static int READ_ALLOCATION = 0x04;
+ public final static int WRITE_CPU = 0x08;
+ public final static int WRITE_GPU = 0x10;
+ public final static int WRITE_ALLOCATION = 0x20;
+
+ private final static int ACCESS_UNKNOWN = 0x00;
+
+ private final int mElementId;
+ private final int mDimensions;
+ private final int mAccessHints;
+ private final Class> mClass;
+
+ private static SimpleCache mTypeCache =
+ new SimpleCache(64);
+
+ /**
+ * Constructs a wild-card FrameType that matches any other FrameType.
+ * @return The wild-card FrameType instance.
+ */
+ public static FrameType any() {
+ return FrameType.fetchType(ELEMENT_DONTCARE, -1, ACCESS_UNKNOWN);
+ }
+
+ /**
+ * Constructs an object-based single FrameType that matches object-based FrameTypes of any
+ * class.
+ * @return A single object-based FrameType instance.
+ */
+ public static FrameType single() {
+ return FrameType.fetchType(null, 0);
+ }
+
+ /**
+ * Constructs an object-based single FrameType of the specified class.
+ * @param clazz The class of the FrameType.
+ * @return A single object-base FrameType instance of the specified class.
+ */
+ public static FrameType single(Class> clazz) {
+ return FrameType.fetchType(clazz, 0);
+ }
+
+ /**
+ * Constructs an object-based array FrameType that matches object-based FrameTypes of any class.
+ * @return An array object-based FrameType instance.
+ */
+ public static FrameType array() {
+ return FrameType.fetchType(null, 1);
+ }
+
+ /**
+ * Constructs an object-based array FrameType with elements of the specified class.
+ * @param clazz The class of the array elements (not the array type).
+ * @return An array object-based FrameType instance of the specified class.
+ */
+ public static FrameType array(Class> clazz) {
+ return FrameType.fetchType(clazz, 1);
+ }
+
+ /**
+ * Constructs a one-dimensional buffer type of the specified element.
+ * @param elementType One of the {@code ELEMENT} constants.
+ * @return A 1D buffer FrameType instance.
+ */
+ public static FrameType buffer1D(int elementType) {
+ return FrameType.fetchType(elementType, 1, ACCESS_UNKNOWN);
+ }
+
+ /**
+ * Constructs a two-dimensional buffer type of the specified element.
+ * @param elementType One of the {@code ELEMENT} constants.
+ * @return A 2D buffer FrameType instance.
+ */
+ public static FrameType buffer2D(int elementType) {
+ return FrameType.fetchType(elementType, 2, ACCESS_UNKNOWN);
+ }
+
+ /**
+ * Constructs a two-dimensional image type of the specified element.
+ * @param elementType One of the {@code ELEMENT} constants.
+ * @param accessHint A bit-mask of access flags (see {@code READ} and {@code WRITE} constants).
+ * @return A 2D image FrameType instance.
+ */
+ public static FrameType image2D(int elementType, int accessHint) {
+ return FrameType.fetchType(elementType, 2, accessHint);
+ }
+
+ /**
+ * Converts the current array type to a single type.
+ * The type must be an object-based type. If the type is already a single type, this does
+ * nothing.
+ * @return type as a single type.
+ */
+ public FrameType asSingle() {
+ if (mElementId != ELEMENT_OBJECT) {
+ throw new RuntimeException("Calling asSingle() on non-object type!");
+ }
+ return FrameType.fetchType(mClass, 0);
+ }
+
+ /**
+ * Converts the current single type to an array type.
+ * The type must be an object-based type. If the type is already an array type, this does
+ * nothing.
+ * @return type as an array type.
+ */
+ public FrameType asArray() {
+ if (mElementId != ELEMENT_OBJECT) {
+ throw new RuntimeException("Calling asArray() on non-object type!");
+ }
+ return FrameType.fetchType(mClass, 1);
+ }
+
+ /**
+ * Returns the FrameType's class specifier, or null if no class was set or the receiver is not
+ * an object-based type.
+ * @return The FrameType's class specifier or null.
+ */
+ public Class> getContentClass() {
+ return mClass;
+ }
+
+ /**
+ * Returns the FrameType's element id.
+ * @return The element id constant.
+ */
+ public int getElementId() {
+ return mElementId;
+ }
+
+ /**
+ * Returns the number of bytes of the FrameType's element, or 0 if no such size can be
+ * determined.
+ * @return The number of bytes of the FrameType's element.
+ */
+ public int getElementSize() {
+ switch (mElementId) {
+ case ELEMENT_INT8:
+ return 1;
+ case ELEMENT_INT16:
+ return 2;
+ case ELEMENT_INT32:
+ case ELEMENT_FLOAT32:
+ case ELEMENT_RGBA8888:
+ return 4;
+ case ELEMENT_INT64:
+ case ELEMENT_FLOAT64:
+ return 4;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the access hints bit-mask of the FrameType.
+ * @return The access hints bit-mask of the FrameType.
+ */
+ public int getAccessHints() {
+ return mAccessHints;
+ }
+
+ /**
+ * Returns the number of dimensions of the FrameType or -1 if no dimensions were set.
+ * @return The number of dimensions of the FrameType.
+ */
+ public int getNumberOfDimensions() {
+ return mDimensions;
+ }
+
+ /**
+ * Returns true, if the FrameType is fully specified.
+ *
+ * A FrameType is fully specified if its element and dimensions are specified.
+ *
+ * @return true, if the FrameType is fully specified.
+ */
+ public boolean isSpecified() {
+ return mElementId != ELEMENT_DONTCARE && mDimensions >= 0;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof FrameType) {
+ FrameType type = (FrameType) object;
+ return mElementId == type.mElementId && mDimensions == type.mDimensions
+ && mAccessHints == type.mAccessHints && mClass == type.mClass;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mElementId ^ mDimensions ^ mAccessHints ^ mClass.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ String result = elementToString(mElementId, mClass) + "[" + mDimensions + "]";
+ if ((mAccessHints & READ_CPU) != 0) {
+ result += "(rcpu)";
+ }
+ if ((mAccessHints & READ_GPU) != 0) {
+ result += "(rgpu)";
+ }
+ if ((mAccessHints & READ_ALLOCATION) != 0) {
+ result += "(ralloc)";
+ }
+ if ((mAccessHints & WRITE_CPU) != 0) {
+ result += "(wcpu)";
+ }
+ if ((mAccessHints & WRITE_GPU) != 0) {
+ result += "(wgpu)";
+ }
+ if ((mAccessHints & WRITE_ALLOCATION) != 0) {
+ result += "(walloc)";
+ }
+ return result;
+ }
+
+ String keyString() {
+ return keyValueForType(mElementId, mDimensions, mAccessHints, mClass);
+ }
+
+ static FrameType tryMerge(FrameType writer, FrameType reader) {
+ if (writer.mElementId == ELEMENT_DONTCARE) {
+ return reader;
+ } else if (reader.mElementId == ELEMENT_DONTCARE) {
+ return writer;
+ } else if (writer.mElementId == ELEMENT_OBJECT && reader.mElementId == ELEMENT_OBJECT) {
+ return tryMergeObjectTypes(writer, reader);
+ } else if (writer.mDimensions > 0 && writer.mElementId == reader.mElementId) {
+ return tryMergeBuffers(writer, reader);
+ } else {
+ return null;
+ }
+ }
+
+ static FrameType tryMergeObjectTypes(FrameType writer, FrameType reader) {
+ int dimensions = Math.max(writer.mDimensions, reader.mDimensions);
+ Class> mergedClass = mergeClasses(writer.mClass, reader.mClass);
+ boolean success = mergedClass != null || writer.mClass == null;
+ return success ? FrameType.fetchType(mergedClass, dimensions) : null;
+ }
+
+ static FrameType tryMergeBuffers(FrameType writer, FrameType reader) {
+ if (writer.mDimensions == reader.mDimensions) {
+ int accessHints = writer.mAccessHints | reader.mAccessHints;
+ return FrameType.fetchType(writer.mElementId, writer.mDimensions, accessHints);
+ }
+ return null;
+ }
+
+ static FrameType merge(FrameType writer, FrameType reader) {
+ FrameType result = tryMerge(writer, reader);
+ if (result == null) {
+ throw new RuntimeException(
+ "Incompatible types in connection: " + writer + " vs. " + reader + "!");
+ }
+ return result;
+ }
+
+ private static String keyValueForType(int elemId, int dims, int hints, Class> clazz) {
+ return elemId + ":" + dims + ":" + hints + ":" + (clazz != null ? clazz.getName() : "0");
+ }
+
+ private static String elementToString(int elemId, Class> clazz) {
+ switch (elemId) {
+ case ELEMENT_INT8:
+ return "int8";
+ case ELEMENT_INT16:
+ return "int16";
+ case ELEMENT_INT32:
+ return "int32";
+ case ELEMENT_INT64:
+ return "int64";
+ case ELEMENT_FLOAT32:
+ return "float32";
+ case ELEMENT_FLOAT64:
+ return "float64";
+ case ELEMENT_RGBA8888:
+ return "rgba8888";
+ case ELEMENT_OBJECT:
+ return "<" + (clazz == null ? "*" : clazz.getSimpleName()) + ">";
+ case ELEMENT_DONTCARE:
+ return "*";
+ default:
+ return "?";
+ }
+ }
+
+ private static Class> mergeClasses(Class> classA, Class> classB) {
+ // Return the most specialized class.
+ if (classA == null) {
+ return classB;
+ } else if (classB == null) {
+ return classA;
+ } else if (classA.isAssignableFrom(classB)) {
+ return classB;
+ } else if (classB.isAssignableFrom(classA)) {
+ return classA;
+ } else {
+ return null;
+ }
+ }
+
+ private static FrameType fetchType(int elementId, int dimensions, int accessHints) {
+ return fetchType(elementId, dimensions, accessHints, null);
+ }
+
+ private static FrameType fetchType(Class> clazz, int dimensions) {
+ return fetchType(ELEMENT_OBJECT, dimensions, ACCESS_UNKNOWN, clazz);
+ }
+
+ private static FrameType fetchType(
+ int elementId, int dimensions, int accessHints, Class> clazz) {
+ String typeKey = FrameType.keyValueForType(elementId, dimensions, accessHints, clazz);
+ FrameType type = mTypeCache.get(typeKey);
+ if (type == null) {
+ type = new FrameType(elementId, dimensions, accessHints, clazz);
+ mTypeCache.put(typeKey, type);
+ }
+ return type;
+ }
+
+ private FrameType(int elementId, int dimensions, int accessHints, Class> clazz) {
+ mElementId = elementId;
+ mDimensions = dimensions;
+ mClass = clazz;
+ mAccessHints = accessHints;
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValue.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValue.java
new file mode 100644
index 0000000000000..fb007e22efdfa
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValue.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import androidx.media.filterfw.BackingStore.Backing;
+
+public class FrameValue extends Frame {
+
+ public Object getValue() {
+ Object result = mBackingStore.lockData(MODE_READ, BackingStore.ACCESS_OBJECT);
+ mBackingStore.unlock();
+ return result;
+ }
+
+ public void setValue(Object value) {
+ Backing backing = mBackingStore.lockBacking(MODE_WRITE, BackingStore.ACCESS_OBJECT);
+ backing.setData(value);
+ mBackingStore.unlock();
+ }
+
+ static FrameValue create(BackingStore backingStore) {
+ assertObjectBased(backingStore.getFrameType());
+ return new FrameValue(backingStore);
+ }
+
+ FrameValue(BackingStore backingStore) {
+ super(backingStore);
+ }
+
+ static void assertObjectBased(FrameType type) {
+ if (type.getElementId() != FrameType.ELEMENT_OBJECT) {
+ throw new RuntimeException("Cannot access non-object based Frame as FrameValue!");
+ }
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValues.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValues.java
new file mode 100644
index 0000000000000..fbddcb1116295
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValues.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import java.lang.reflect.Array;
+
+public class FrameValues extends FrameValue {
+
+ /**
+ * Returns the number of values in the Frame.
+ *
+ * This returns 1, if the Frame value is null, or if the value is not an array.
+ *
+ * @return The number of values in the Frame.
+ */
+ public int getCount() {
+ Object value = super.getValue();
+ if (value == null || !value.getClass().isArray()) {
+ return 1;
+ } else {
+ return Array.getLength(super.getValue());
+ }
+ }
+
+ /**
+ * Returns the values in the Frame as an array.
+ *
+ * Note, that this may be called on Frames that have a non-array object assigned to them. In
+ * that case, this method will wrap the object in an array and return that. This way, filters
+ * can treat any object based frame as arrays.
+ *
+ * @return The array of values in this frame.
+ */
+ public Object getValues() {
+ Object value = super.getValue();
+ if (value == null || value.getClass().isArray()) {
+ return super.getValue();
+ } else {
+ // Allow reading a single as an array.
+ Object[] array = (Object[])Array.newInstance(value.getClass(), 1);
+ array[0] = value;
+ return array;
+ }
+ }
+
+ /**
+ * Returns the value at the specified index.
+ *
+ * In case the value is null or not an array, the index must be 0, and the value itself is
+ * returned.
+ *
+ * @param index The index to access.
+ * @return The value at that index.
+ */
+ public Object getValueAtIndex(int index) {
+ Object value = super.getValue();
+ if (value == null || !value.getClass().isArray()) {
+ if (index != 0) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ } else {
+ return value;
+ }
+ } else {
+ return Array.get(value, index);
+ }
+ }
+
+ /**
+ * Returns the value as a FrameValue at the specified index.
+ *
+ * Use this if you want to access elements as FrameValues. You must release the result when
+ * you are done using it.
+ *
+ * @param index The index to access.
+ * @return The value as a FrameValue at that index (must release).
+ */
+ public FrameValue getFrameValueAtIndex(int index) {
+ Object value = getValueAtIndex(index);
+ FrameValue result = Frame.create(getType().asSingle(), new int[0]).asFrameValue();
+ result.setValue(value);
+ return result;
+ }
+
+ /**
+ * Assign the array of values to the frame.
+ *
+ * You may assign null or a non-array object, which are interpreted as a 1-length array.
+ *
+ * @param values The values to assign to the frame.
+ */
+ public void setValues(Object values) {
+ super.setValue(values);
+ }
+
+ /**
+ * Assign a value at the specified index.
+ *
+ * In case the held value is not an array, the index must be 0, and the object will be replaced
+ * by the new object.
+ *
+ * @param value The value to assign.
+ * @param index The index to assign to.
+ */
+ public void setValueAtIndex(Object value, int index) {
+ super.assertAccessible(MODE_WRITE);
+ Object curValue = super.getValue();
+ if (curValue == null || !curValue.getClass().isArray()) {
+ if (index != 0) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ } else {
+ curValue = value;
+ }
+ } else {
+ Array.set(curValue, index, value);
+ }
+ }
+
+ /**
+ * Assign a FrameValue's value at the specified index.
+ *
+ * This method unpacks the FrameValue and assigns the unpacked value to the specified index.
+ * This does not affect the retain-count of the passed Frame.
+ *
+ * @param frame The frame value to assign.
+ * @param index The index to assign to.
+ */
+ public void setFrameValueAtIndex(FrameValue frame, int index) {
+ Object value = frame.getValue();
+ setValueAtIndex(value, index);
+ }
+
+ static FrameValues create(BackingStore backingStore) {
+ assertObjectBased(backingStore.getFrameType());
+ return new FrameValues(backingStore);
+ }
+
+ FrameValues(BackingStore backingStore) {
+ super(backingStore);
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GLToolbox.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GLToolbox.java
new file mode 100644
index 0000000000000..1c3c7e9c12288
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GLToolbox.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.graphics.Bitmap;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.os.Looper;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * TODO: Make this package-private as RenderTarget and TextureSource should suffice as public
+ * facing OpenGL utilities.
+ * @hide
+ */
+public class GLToolbox {
+
+ public static int textureNone() {
+ return 0;
+ }
+
+ public static boolean isTexture(int texId) {
+ return GLES20.glIsTexture(texId);
+ }
+
+ public static void deleteTexture(int texId) {
+ int[] textures = new int[] { texId };
+ assertNonUiThread("glDeleteTextures");
+ GLES20.glDeleteTextures(1, textures, 0);
+ checkGlError("glDeleteTextures");
+ }
+
+ public static void deleteFbo(int fboId) {
+ int[] fbos = new int[] { fboId };
+ assertNonUiThread("glDeleteFramebuffers");
+ GLES20.glDeleteFramebuffers(1, fbos, 0);
+ checkGlError("glDeleteFramebuffers");
+ }
+
+ public static int generateTexture() {
+ int[] textures = new int[1];
+ GLES20.glGenTextures(1, textures, 0);
+ checkGlError("glGenTextures");
+ return textures[0];
+ }
+
+ public static int generateFbo() {
+ int[] fbos = new int[1];
+ GLES20.glGenFramebuffers(1, fbos, 0);
+ checkGlError("glGenFramebuffers");
+ return fbos[0];
+ }
+
+ public static void readFbo(int fboId, ByteBuffer pixels, int width, int height) {
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId);
+ GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixels);
+ checkGlError("glReadPixels");
+ }
+
+ public static void readTarget(RenderTarget target, ByteBuffer pixels, int width, int height) {
+ target.focus();
+ GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixels);
+ checkGlError("glReadPixels");
+ }
+
+ public static int attachedTexture(int fboId) {
+ int[] params = new int[1];
+ GLES20.glGetFramebufferAttachmentParameteriv(
+ GLES20.GL_FRAMEBUFFER,
+ GLES20.GL_COLOR_ATTACHMENT0,
+ GLES20.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
+ params, 0);
+ checkGlError("glGetFramebufferAttachmentParameteriv");
+ return params[0];
+ }
+
+ public static void attachTextureToFbo(int texId, int fboId) {
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId);
+ GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,
+ GLES20.GL_COLOR_ATTACHMENT0,
+ GLES20.GL_TEXTURE_2D,
+ texId,
+ 0);
+ checkGlError("glFramebufferTexture2D");
+ }
+
+ public static void allocateTexturePixels(int texId, int target, int width, int height) {
+ setTexturePixels(texId, target, (ByteBuffer)null, width, height);
+ }
+
+ public static void setTexturePixels(int texId, int target, Bitmap bitmap) {
+ GLES20.glBindTexture(target, texId);
+ GLUtils.texImage2D(target, 0, bitmap, 0);
+ checkGlError("glTexImage2D");
+ setDefaultTexParams();
+ }
+
+ public static void setTexturePixels(int texId, int target, ByteBuffer pixels,
+ int width, int height) {
+ GLES20.glBindTexture(target, texId);
+
+ // For some devices, "pixels" being null causes system error.
+ if (pixels == null) {
+ pixels = ByteBuffer.allocateDirect(width * height * 4);
+ }
+ GLES20.glTexImage2D(target, 0, GLES20.GL_RGBA, width, height, 0,
+ GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixels);
+ checkGlError("glTexImage2D");
+ setDefaultTexParams();
+ }
+
+ public static void setDefaultTexParams() {
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
+ GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
+ GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
+ GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
+ GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ checkGlError("glTexParameteri");
+ }
+
+ public static int vboNone() {
+ return 0;
+ }
+
+ public static int generateVbo() {
+ int[] vbos = new int[1];
+ GLES20.glGenBuffers(1, vbos, 0);
+ checkGlError("glGenBuffers");
+ return vbos[0];
+ }
+
+ public static void setVboData(int vboId, ByteBuffer data) {
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId);
+ GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, data.remaining(), data, GLES20.GL_STATIC_DRAW);
+ checkGlError("glBufferData");
+ }
+
+ public static void setVboFloats(int vboId, float[] values) {
+ int len = values.length * 4;
+ ByteBuffer buffer = ByteBuffer.allocateDirect(len).order(ByteOrder.nativeOrder());
+ setVboData(vboId, buffer);
+ }
+
+ public static boolean isVbo(int vboId) {
+ return GLES20.glIsBuffer(vboId);
+ }
+
+ public static void deleteVbo(int vboId) {
+ int[] buffers = new int[] { vboId };
+ GLES20.glDeleteBuffers(1, buffers, 0);
+ checkGlError("glDeleteBuffers");
+ }
+
+ public static void checkGlError(String operation) {
+ int error;
+ while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ throw new RuntimeException("GL Operation '" + operation + "' caused error "
+ + Integer.toHexString(error) + "!");
+ }
+ }
+
+ /**
+ * Make sure we are not operating in the UI thread.
+ *
+ * It is often tricky to track down bugs that happen when issuing GL commands in the UI thread.
+ * This is especially true when releasing GL resources. Often this will cause errors much later
+ * on. Therefore we make sure we do not do these dangerous operations on the UI thread.
+ */
+ private static void assertNonUiThread(String operation) {
+ if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
+ throw new RuntimeException("Attempting to perform GL operation '" + operation
+ + "' on UI thread!");
+ }
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphExporter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphExporter.java
new file mode 100644
index 0000000000000..001396527267f
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphExporter.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This class provides functions to export a FilterGraph.
+
+package androidx.media.filterfw;
+
+import android.content.Context;
+
+import java.io.FileOutputStream;
+import java.io.OutputStreamWriter;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * This class provides functions to export a FilterGraph as a DOT file.
+ */
+public class GraphExporter {
+
+ /**
+ * Exports the graph as DOT (see http://en.wikipedia.org/wiki/DOT_language).
+ * Using the exported file, the graph can be visualized e.g. with the command line tool dot.
+ * Optionally, one may /exclude/ unconnected optional ports (third parameter = false),
+ * since they can quickly clutter the visualization (and, depending on the purpose, may not
+ * be interesting).
+ *
+ * Example workflow:
+ * 1. run application on device, make sure it calls exportGraphAsDOT(...);
+ * 2. adb pull /data/data//files/.gv graph.gv
+ * 3. dot -Tpng graph.gv -o graph.png
+ * 4. eog graph.png
+ */
+ static public void exportAsDot(FilterGraph graph, String filename,
+ boolean includeUnconnectedOptionalPorts)
+ throws java.io.FileNotFoundException, java.io.IOException {
+ // Initialize, open file stream
+ Context myAppContext = graph.getContext().getApplicationContext();
+ Filter[] filters = graph.getAllFilters();
+ FileOutputStream fOut = myAppContext.openFileOutput(filename, Context.MODE_PRIVATE);
+ OutputStreamWriter dotFile = new OutputStreamWriter(fOut);
+
+ // Write beginning of DOT file
+ dotFile.write("digraph graphname {\n");
+ dotFile.write(" node [shape=record];\n");
+
+ // N.B. For specification and lots of examples of the DOT language, see
+ // http://www.graphviz.org/Documentation/dotguide.pdf
+
+ // Iterate over all filters of the graph, write corresponding DOT node elements
+
+ for(Filter filter : filters) {
+ dotFile.write(getDotName(" " + filter.getName()) + " [label=\"{");
+
+ // Write upper part of element (i.e., input ports)
+ Set inputPorts = getInputPorts(filter, includeUnconnectedOptionalPorts);
+ if(inputPorts.size() > 0) {
+ dotFile.write(" { ");
+ int counter = 0;
+ for(String p : inputPorts) {
+ dotFile.write("<" + getDotName(p) + "_IN>" + p);
+ if(++counter != inputPorts.size()) dotFile.write(" | ");
+ }
+ dotFile.write(" } | ");
+ }
+
+ // Write center part of element (i.e., element label)
+ dotFile.write(filter.getName());
+
+ // Write lower part of element (i.e., output ports)
+ Set outputPorts = getOutputPorts(filter, includeUnconnectedOptionalPorts);
+ if(outputPorts.size() > 0) {
+ dotFile.write(" | { ");
+ int counter = 0;
+ for(String p : outputPorts) {
+ dotFile.write("<" + getDotName(p) + "_OUT>" + p);
+ if(++counter != outputPorts.size()) dotFile.write(" | ");
+ }
+ dotFile.write(" } ");
+ }
+
+ dotFile.write("}\"];\n");
+ }
+ dotFile.write("\n");
+
+ // Iterate over all filters again to collect connections and find unconnected ports
+
+ int dummyNodeCounter = 0;
+ for(Filter filter : filters) {
+ Set outputPorts = getOutputPorts(filter, includeUnconnectedOptionalPorts);
+ for(String portName : outputPorts) {
+ OutputPort source = filter.getConnectedOutputPort(portName);
+ if(source != null) {
+ // Found a connection, draw it
+ InputPort target = source.getTarget();
+ dotFile.write(" " +
+ getDotName(source.getFilter().getName()) + ":" +
+ getDotName(source.getName()) + "_OUT -> " +
+ getDotName(target.getFilter().getName()) + ":" +
+ getDotName(target.getName()) + "_IN;\n" );
+ } else {
+ // Found a unconnected output port, add dummy node
+ String color = filter.getSignature().getOutputPortInfo(portName).isRequired()
+ ? "red" : "blue"; // red for unconnected, required ports
+ dotFile.write(" " +
+ "dummy" + (++dummyNodeCounter) +
+ " [shape=point,label=\"\",color=" + color + "];\n" +
+ " " + getDotName(filter.getName()) + ":" +
+ getDotName(portName) + "_OUT -> " +
+ "dummy" + dummyNodeCounter + " [color=" + color + "];\n");
+ }
+ }
+
+ Set inputPorts = getInputPorts(filter, includeUnconnectedOptionalPorts);
+ for(String portName : inputPorts) {
+ InputPort target = filter.getConnectedInputPort(portName);
+ if(target != null) {
+ // Found a connection -- nothing to do, connections have been written out above
+ } else {
+ // Found a unconnected input port, add dummy node
+ String color = filter.getSignature().getInputPortInfo(portName).isRequired()
+ ? "red" : "blue"; // red for unconnected, required ports
+ dotFile.write(" " +
+ "dummy" + (++dummyNodeCounter) +
+ " [shape=point,label=\"\",color=" + color + "];\n" +
+ " dummy" + dummyNodeCounter + " -> " +
+ getDotName(filter.getName()) + ":" +
+ getDotName(portName) + "_IN [color=" + color + "];\n");
+ }
+ }
+ }
+
+ // Write end of DOT file, close file stream
+ dotFile.write("}\n");
+ dotFile.flush();
+ dotFile.close();
+ }
+
+ // Internal methods
+
+ // From element's name in XML, create DOT-allowed element name
+ static private String getDotName(String raw) {
+ return raw.replaceAll("\\.", "___"); // DOT does not allow . in element names
+ }
+
+ // Retrieve all input ports of a filter, including:
+ // unconnected ports (which can not be retrieved from the filter, only from the signature), and
+ // additional (connected) ports not listed in the signature (which is allowed by default,
+ // unless disallowOtherInputs is defined in signature).
+ // With second parameter = false, *omit* unconnected optional ports.
+ static private Set getInputPorts(Filter filter, boolean includeUnconnectedOptional) {
+ // add (connected) ports from filter
+ Set ports = new HashSet();
+ ports.addAll(filter.getConnectedInputPortMap().keySet());
+
+ // add (unconnected) ports from signature
+ HashMap signaturePorts = filter.getSignature().getInputPorts();
+ if(signaturePorts != null){
+ for(Entry e : signaturePorts.entrySet()) {
+ if(includeUnconnectedOptional || e.getValue().isRequired()) {
+ ports.add(e.getKey());
+ }
+ }
+ }
+ return ports;
+ }
+
+ // Retrieve all output ports of a filter (analogous to above function)
+ static private Set getOutputPorts(Filter filter, boolean includeUnconnectedOptional) {
+ // add (connected) ports from filter
+ Set ports = new HashSet();
+ ports.addAll(filter.getConnectedOutputPortMap().keySet());
+
+ // add (unconnected) ports from signature
+ HashMap signaturePorts = filter.getSignature().getOutputPorts();
+ if(signaturePorts != null){
+ for(Entry e : signaturePorts.entrySet()) {
+ if(includeUnconnectedOptional || e.getValue().isRequired()) {
+ ports.add(e.getKey());
+ }
+ }
+ }
+ return ports;
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphInputSource.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphInputSource.java
new file mode 100644
index 0000000000000..03b3abee9478a
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphInputSource.java
@@ -0,0 +1,58 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package androidx.media.filterpacks.base;
+
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.Frame;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.Signature;
+
+public class GraphInputSource extends Filter {
+
+ private Frame mFrame = null;
+
+ public GraphInputSource(MffContext context, String name) {
+ super(context, name);
+ }
+
+ @Override
+ public Signature getSignature() {
+ return new Signature()
+ .addOutputPort("frame", Signature.PORT_REQUIRED, FrameType.any())
+ .disallowOtherInputs();
+ }
+
+ public void pushFrame(Frame frame) {
+ if (mFrame != null) {
+ mFrame.release();
+ }
+ if (frame == null) {
+ throw new RuntimeException("Attempting to assign null-frame!");
+ }
+ mFrame = frame.retain();
+ }
+
+ @Override
+ protected void onProcess() {
+ if (mFrame != null) {
+ getConnectedOutputPort("frame").pushFrame(mFrame);
+ mFrame.release();
+ mFrame = null;
+ }
+ }
+
+ @Override
+ protected void onTearDown() {
+ if (mFrame != null) {
+ mFrame.release();
+ mFrame = null;
+ }
+ }
+
+ @Override
+ protected boolean canSchedule() {
+ return super.canSchedule() && mFrame != null;
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphOutputTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphOutputTarget.java
new file mode 100644
index 0000000000000..1f3be101e0919
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphOutputTarget.java
@@ -0,0 +1,60 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package androidx.media.filterpacks.base;
+
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.Frame;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.Signature;
+
+public class GraphOutputTarget extends Filter {
+
+ private Frame mFrame = null;
+ private FrameType mType = FrameType.any();
+
+ public GraphOutputTarget(MffContext context, String name) {
+ super(context, name);
+ }
+
+ // TODO: During initialization only?
+ public void setType(FrameType type) {
+ mType = type;
+ }
+
+ public FrameType getType() {
+ return mType;
+ }
+
+ @Override
+ public Signature getSignature() {
+ return new Signature()
+ .addInputPort("frame", Signature.PORT_REQUIRED, mType)
+ .disallowOtherInputs();
+ }
+
+ // Returns a retained frame!
+ public Frame pullFrame() {
+ Frame result = null;
+ if (mFrame != null) {
+ result = mFrame;
+ mFrame = null;
+ }
+ return result;
+ }
+
+ @Override
+ protected void onProcess() {
+ Frame frame = getConnectedInputPort("frame").pullFrame();
+ if (mFrame != null) {
+ mFrame.release();
+ }
+ mFrame = frame.retain();
+ }
+
+ @Override
+ protected boolean canSchedule() {
+ return super.canSchedule() && mFrame == null;
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphReader.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphReader.java
new file mode 100644
index 0000000000000..ef885e369f7cb
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphReader.java
@@ -0,0 +1,576 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.text.TextUtils;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * A GraphReader allows obtaining filter graphs from XML graph files or strings.
+ */
+public class GraphReader {
+
+ private static interface Command {
+ public void execute(CommandStack stack);
+ }
+
+ private static class CommandStack {
+ private ArrayList mCommands = new ArrayList();
+ private FilterGraph.Builder mBuilder;
+ private FilterFactory mFactory;
+ private MffContext mContext;
+
+ public CommandStack(MffContext context) {
+ mContext = context;
+ mBuilder = new FilterGraph.Builder(mContext);
+ mFactory = new FilterFactory();
+ }
+
+ public void execute() {
+ for (Command command : mCommands) {
+ command.execute(this);
+ }
+ }
+
+ public void append(Command command) {
+ mCommands.add(command);
+ }
+
+ public FilterFactory getFactory() {
+ return mFactory;
+ }
+
+ public MffContext getContext() {
+ return mContext;
+ }
+
+ protected FilterGraph.Builder getBuilder() {
+ return mBuilder;
+ }
+ }
+
+ private static class ImportPackageCommand implements Command {
+ private String mPackageName;
+
+ public ImportPackageCommand(String packageName) {
+ mPackageName = packageName;
+ }
+
+ @Override
+ public void execute(CommandStack stack) {
+ try {
+ stack.getFactory().addPackage(mPackageName);
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+ }
+
+ private static class AddLibraryCommand implements Command {
+ private String mLibraryName;
+
+ public AddLibraryCommand(String libraryName) {
+ mLibraryName = libraryName;
+ }
+
+ @Override
+ public void execute(CommandStack stack) {
+ FilterFactory.addFilterLibrary(mLibraryName);
+ }
+ }
+
+ private static class AllocateFilterCommand implements Command {
+ private String mClassName;
+ private String mFilterName;
+
+ public AllocateFilterCommand(String className, String filterName) {
+ mClassName = className;
+ mFilterName = filterName;
+ }
+
+ @Override
+ public void execute(CommandStack stack) {
+ Filter filter = null;
+ try {
+ filter = stack.getFactory().createFilterByClassName(mClassName,
+ mFilterName,
+ stack.getContext());
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeException("Error creating filter " + mFilterName + "!", e);
+ }
+ stack.getBuilder().addFilter(filter);
+ }
+ }
+
+ private static class AddSourceSlotCommand implements Command {
+ private String mName;
+ private String mSlotName;
+
+ public AddSourceSlotCommand(String name, String slotName) {
+ mName = name;
+ mSlotName = slotName;
+ }
+
+ @Override
+ public void execute(CommandStack stack) {
+ stack.getBuilder().addFrameSlotSource(mName, mSlotName);
+ }
+ }
+
+ private static class AddTargetSlotCommand implements Command {
+ private String mName;
+ private String mSlotName;
+
+ public AddTargetSlotCommand(String name, String slotName) {
+ mName = name;
+ mSlotName = slotName;
+ }
+
+ @Override
+ public void execute(CommandStack stack) {
+ stack.getBuilder().addFrameSlotTarget(mName, mSlotName);
+ }
+ }
+
+ private static class AddVariableCommand implements Command {
+ private String mName;
+ private Object mValue;
+
+ public AddVariableCommand(String name, Object value) {
+ mName = name;
+ mValue = value;
+ }
+
+ @Override
+ public void execute(CommandStack stack) {
+ stack.getBuilder().addVariable(mName, mValue);
+ }
+ }
+
+ private static class SetFilterInputCommand implements Command {
+ private String mFilterName;
+ private String mFilterInput;
+ private Object mValue;
+
+ public SetFilterInputCommand(String filterName, String input, Object value) {
+ mFilterName = filterName;
+ mFilterInput = input;
+ mValue = value;
+ }
+
+ @Override
+ public void execute(CommandStack stack) {
+ if (mValue instanceof Variable) {
+ String varName = ((Variable)mValue).name;
+ stack.getBuilder().assignVariableToFilterInput(varName, mFilterName, mFilterInput);
+ } else {
+ stack.getBuilder().assignValueToFilterInput(mValue, mFilterName, mFilterInput);
+ }
+ }
+ }
+
+ private static class ConnectCommand implements Command {
+ private String mSourceFilter;
+ private String mSourcePort;
+ private String mTargetFilter;
+ private String mTargetPort;
+
+ public ConnectCommand(String sourceFilter,
+ String sourcePort,
+ String targetFilter,
+ String targetPort) {
+ mSourceFilter = sourceFilter;
+ mSourcePort = sourcePort;
+ mTargetFilter = targetFilter;
+ mTargetPort = targetPort;
+ }
+
+ @Override
+ public void execute(CommandStack stack) {
+ stack.getBuilder().connect(mSourceFilter, mSourcePort, mTargetFilter, mTargetPort);
+ }
+ }
+
+ private static class Variable {
+ public String name;
+
+ public Variable(String name) {
+ this.name = name;
+ }
+ }
+
+ private static class XmlGraphReader {
+
+ private SAXParserFactory mParserFactory;
+
+ private static class GraphDataHandler extends DefaultHandler {
+
+ private CommandStack mCommandStack;
+ private boolean mInGraph = false;
+ private String mCurFilterName = null;
+
+ public GraphDataHandler(CommandStack commandStack) {
+ mCommandStack = commandStack;
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attr)
+ throws SAXException {
+ if (localName.equals("graph")) {
+ beginGraph();
+ } else {
+ assertInGraph(localName);
+ if (localName.equals("import")) {
+ addImportCommand(attr);
+ } else if (localName.equals("library")) {
+ addLibraryCommand(attr);
+ } else if (localName.equals("connect")) {
+ addConnectCommand(attr);
+ } else if (localName.equals("var")) {
+ addVarCommand(attr);
+ } else if (localName.equals("filter")) {
+ beginFilter(attr);
+ } else if (localName.equals("input")) {
+ addFilterInput(attr);
+ } else {
+ throw new SAXException("Unknown XML element '" + localName + "'!");
+ }
+ }
+ }
+
+ @Override
+ public void endElement (String uri, String localName, String qName) {
+ if (localName.equals("graph")) {
+ endGraph();
+ } else if (localName.equals("filter")) {
+ endFilter();
+ }
+ }
+
+ private void addImportCommand(Attributes attributes) throws SAXException {
+ String packageName = getRequiredAttribute(attributes, "package");
+ mCommandStack.append(new ImportPackageCommand(packageName));
+ }
+
+ private void addLibraryCommand(Attributes attributes) throws SAXException {
+ String libraryName = getRequiredAttribute(attributes, "name");
+ mCommandStack.append(new AddLibraryCommand(libraryName));
+ }
+
+ private void addConnectCommand(Attributes attributes) {
+ String sourcePortName = null;
+ String sourceFilterName = null;
+ String targetPortName = null;
+ String targetFilterName = null;
+
+ // check for shorthand:
+ String sourceTag = attributes.getValue("source");
+ if (sourceTag != null) {
+ String[] sourceParts = sourceTag.split(":");
+ if (sourceParts.length == 2) {
+ sourceFilterName = sourceParts[0];
+ sourcePortName = sourceParts[1];
+ } else {
+ throw new RuntimeException(
+ "'source' tag needs to have format \"filter:port\"! " +
+ "Alternatively, you may use the form " +
+ "'sourceFilter=\"filter\" sourcePort=\"port\"'.");
+ }
+ } else {
+ sourceFilterName = attributes.getValue("sourceFilter");
+ sourcePortName = attributes.getValue("sourcePort");
+ }
+
+ String targetTag = attributes.getValue("target");
+ if (targetTag != null) {
+ String[] targetParts = targetTag.split(":");
+ if (targetParts.length == 2) {
+ targetFilterName = targetParts[0];
+ targetPortName = targetParts[1];
+ } else {
+ throw new RuntimeException(
+ "'target' tag needs to have format \"filter:port\"! " +
+ "Alternatively, you may use the form " +
+ "'targetFilter=\"filter\" targetPort=\"port\"'.");
+ }
+ } else {
+ targetFilterName = attributes.getValue("targetFilter");
+ targetPortName = attributes.getValue("targetPort");
+ }
+
+ String sourceSlotName = attributes.getValue("sourceSlot");
+ String targetSlotName = attributes.getValue("targetSlot");
+ if (sourceSlotName != null) {
+ sourceFilterName = "sourceSlot_" + sourceSlotName;
+ mCommandStack.append(new AddSourceSlotCommand(sourceFilterName,
+ sourceSlotName));
+ sourcePortName = "frame";
+ }
+ if (targetSlotName != null) {
+ targetFilterName = "targetSlot_" + targetSlotName;
+ mCommandStack.append(new AddTargetSlotCommand(targetFilterName,
+ targetSlotName));
+ targetPortName = "frame";
+ }
+ assertValueNotNull("sourceFilter", sourceFilterName);
+ assertValueNotNull("sourcePort", sourcePortName);
+ assertValueNotNull("targetFilter", targetFilterName);
+ assertValueNotNull("targetPort", targetPortName);
+ // TODO: Should slot connections auto-branch?
+ mCommandStack.append(new ConnectCommand(sourceFilterName,
+ sourcePortName,
+ targetFilterName,
+ targetPortName));
+ }
+
+ private void addVarCommand(Attributes attributes) throws SAXException {
+ String varName = getRequiredAttribute(attributes, "name");
+ Object varValue = getAssignmentValue(attributes);
+ mCommandStack.append(new AddVariableCommand(varName, varValue));
+ }
+
+ private void beginGraph() throws SAXException {
+ if (mInGraph) {
+ throw new SAXException("Found more than one graph element in XML!");
+ }
+ mInGraph = true;
+ }
+
+ private void endGraph() {
+ mInGraph = false;
+ }
+
+ private void beginFilter(Attributes attributes) throws SAXException {
+ String className = getRequiredAttribute(attributes, "class");
+ mCurFilterName = getRequiredAttribute(attributes, "name");
+ mCommandStack.append(new AllocateFilterCommand(className, mCurFilterName));
+ }
+
+ private void endFilter() {
+ mCurFilterName = null;
+ }
+
+ private void addFilterInput(Attributes attributes) throws SAXException {
+ // Make sure we are in a filter element
+ if (mCurFilterName == null) {
+ throw new SAXException("Found 'input' element outside of 'filter' "
+ + "element!");
+ }
+
+ // Get input name and value
+ String inputName = getRequiredAttribute(attributes, "name");
+ Object inputValue = getAssignmentValue(attributes);
+ if (inputValue == null) {
+ throw new SAXException("No value specified for input '" + inputName + "' "
+ + "of filter '" + mCurFilterName + "'!");
+ }
+
+ // Push commmand
+ mCommandStack.append(new SetFilterInputCommand(mCurFilterName,
+ inputName,
+ inputValue));
+ }
+
+ private void assertInGraph(String localName) throws SAXException {
+ if (!mInGraph) {
+ throw new SAXException("Encountered '" + localName + "' element outside of "
+ + "'graph' element!");
+ }
+ }
+
+ private static Object getAssignmentValue(Attributes attributes) {
+ String strValue = null;
+ if ((strValue = attributes.getValue("stringValue")) != null) {
+ return strValue;
+ } else if ((strValue = attributes.getValue("booleanValue")) != null) {
+ return Boolean.parseBoolean(strValue);
+ } else if ((strValue = attributes.getValue("intValue")) != null) {
+ return Integer.parseInt(strValue);
+ } else if ((strValue = attributes.getValue("floatValue")) != null) {
+ return Float.parseFloat(strValue);
+ } else if ((strValue = attributes.getValue("floatsValue")) != null) {
+ String[] floatStrings = TextUtils.split(strValue, ",");
+ float[] result = new float[floatStrings.length];
+ for (int i = 0; i < floatStrings.length; ++i) {
+ result[i] = Float.parseFloat(floatStrings[i]);
+ }
+ return result;
+ } else if ((strValue = attributes.getValue("varValue")) != null) {
+ return new Variable(strValue);
+ } else {
+ return null;
+ }
+ }
+
+ private static String getRequiredAttribute(Attributes attributes, String name)
+ throws SAXException {
+ String result = attributes.getValue(name);
+ if (result == null) {
+ throw new SAXException("Required attribute '" + name + "' not found!");
+ }
+ return result;
+ }
+
+ private static void assertValueNotNull(String valueName, Object value) {
+ if (value == null) {
+ throw new NullPointerException("Required value '" + value + "' not specified!");
+ }
+ }
+
+ }
+
+ public XmlGraphReader() {
+ mParserFactory = SAXParserFactory.newInstance();
+ }
+
+ public void parseString(String graphString, CommandStack commandStack) throws IOException {
+ try {
+ XMLReader reader = getReaderForCommandStack(commandStack);
+ reader.parse(new InputSource(new StringReader(graphString)));
+ } catch (SAXException e) {
+ throw new IOException("XML parse error during graph parsing!", e);
+ }
+ }
+
+ public void parseInput(InputStream inputStream, CommandStack commandStack)
+ throws IOException {
+ try {
+ XMLReader reader = getReaderForCommandStack(commandStack);
+ reader.parse(new InputSource(inputStream));
+ } catch (SAXException e) {
+ throw new IOException("XML parse error during graph parsing!", e);
+ }
+ }
+
+ private XMLReader getReaderForCommandStack(CommandStack commandStack) throws IOException {
+ try {
+ SAXParser parser = mParserFactory.newSAXParser();
+ XMLReader reader = parser.getXMLReader();
+ GraphDataHandler graphHandler = new GraphDataHandler(commandStack);
+ reader.setContentHandler(graphHandler);
+ return reader;
+ } catch (ParserConfigurationException e) {
+ throw new IOException("Error creating SAXParser for graph parsing!", e);
+ } catch (SAXException e) {
+ throw new IOException("Error creating XMLReader for graph parsing!", e);
+ }
+ }
+ }
+
+ /**
+ * Read an XML graph from a String.
+ *
+ * This function automatically checks each filters' signatures and throws a Runtime Exception
+ * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
+ *
+ * @param context the MffContext into which to load the graph.
+ * @param xmlSource the graph specified in XML.
+ * @return the FilterGraph instance for the XML source.
+ * @throws IOException if there was an error parsing the source.
+ */
+ public static FilterGraph readXmlGraph(MffContext context, String xmlSource)
+ throws IOException {
+ FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource);
+ return builder.build();
+ }
+
+ /**
+ * Read an XML sub-graph from a String.
+ *
+ * @param context the MffContext into which to load the graph.
+ * @param xmlSource the graph specified in XML.
+ * @param parentGraph the parent graph.
+ * @return the FilterGraph instance for the XML source.
+ * @throws IOException if there was an error parsing the source.
+ */
+ public static FilterGraph readXmlSubGraph(
+ MffContext context, String xmlSource, FilterGraph parentGraph)
+ throws IOException {
+ FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource);
+ return builder.buildSubGraph(parentGraph);
+ }
+
+ /**
+ * Read an XML graph from a resource.
+ *
+ * This function automatically checks each filters' signatures and throws a Runtime Exception
+ * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
+ *
+ * @param context the MffContext into which to load the graph.
+ * @param resourceId the XML resource ID.
+ * @return the FilterGraph instance for the XML source.
+ * @throws IOException if there was an error reading or parsing the resource.
+ */
+ public static FilterGraph readXmlGraphResource(MffContext context, int resourceId)
+ throws IOException {
+ FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId);
+ return builder.build();
+ }
+
+ /**
+ * Read an XML graph from a resource.
+ *
+ * This function automatically checks each filters' signatures and throws a Runtime Exception
+ * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
+ *
+ * @param context the MffContext into which to load the graph.
+ * @param resourceId the XML resource ID.
+ * @return the FilterGraph instance for the XML source.
+ * @throws IOException if there was an error reading or parsing the resource.
+ */
+ public static FilterGraph readXmlSubGraphResource(
+ MffContext context, int resourceId, FilterGraph parentGraph)
+ throws IOException {
+ FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId);
+ return builder.buildSubGraph(parentGraph);
+ }
+
+ private static FilterGraph.Builder getBuilderForXmlString(MffContext context, String source)
+ throws IOException {
+ XmlGraphReader reader = new XmlGraphReader();
+ CommandStack commands = new CommandStack(context);
+ reader.parseString(source, commands);
+ commands.execute();
+ return commands.getBuilder();
+ }
+
+ private static FilterGraph.Builder getBuilderForXmlResource(MffContext context, int resourceId)
+ throws IOException {
+ InputStream inputStream = context.getApplicationContext().getResources()
+ .openRawResource(resourceId);
+ XmlGraphReader reader = new XmlGraphReader();
+ CommandStack commands = new CommandStack(context);
+ reader.parseInput(inputStream, commands);
+ commands.execute();
+ return commands.getBuilder();
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphRunner.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphRunner.java
new file mode 100644
index 0000000000000..36aed63ad614d
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphRunner.java
@@ -0,0 +1,1023 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.os.ConditionVariable;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Stack;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * A GraphRunner schedules and executes the filter nodes of a graph.
+ *
+ * Typically, you create a GraphRunner given a FilterGraph instance, and execute it by calling
+ * {@link #start(FilterGraph)}.
+ *
+ * The scheduling strategy determines how the filter nodes are selected
+ * for scheduling. More precisely, given the set of nodes that can be scheduled, the scheduling
+ * strategy determines which node of this set to select for execution. For instance, an LFU
+ * scheduler (the default) chooses the node that has been executed the least amount of times.
+ */
+public final class GraphRunner {
+
+ private static int PRIORITY_SLEEP = -1;
+ private static int PRIORITY_STOP = -2;
+
+ private static final Event BEGIN_EVENT = new Event(Event.BEGIN, null);
+ private static final Event FLUSH_EVENT = new Event(Event.FLUSH, null);
+ private static final Event HALT_EVENT = new Event(Event.HALT, null);
+ private static final Event KILL_EVENT = new Event(Event.KILL, null);
+ private static final Event PAUSE_EVENT = new Event(Event.PAUSE, null);
+ private static final Event RELEASE_FRAMES_EVENT = new Event(Event.RELEASE_FRAMES, null);
+ private static final Event RESTART_EVENT = new Event(Event.RESTART, null);
+ private static final Event RESUME_EVENT = new Event(Event.RESUME, null);
+ private static final Event STEP_EVENT = new Event(Event.STEP, null);
+ private static final Event STOP_EVENT = new Event(Event.STOP, null);
+
+ private static class State {
+ public static final int STOPPED = 1;
+ public static final int PREPARING = 2;
+ public static final int RUNNING = 4;
+ public static final int PAUSED = 8;
+ public static final int HALTED = 16;
+
+ private int mCurrent = STOPPED;
+
+ public synchronized void setState(int newState) {
+ mCurrent = newState;
+ }
+
+ public synchronized boolean check(int state) {
+ return ((mCurrent & state) == state);
+ }
+
+ public synchronized boolean addState(int state) {
+ if ((mCurrent & state) != state) {
+ mCurrent |= state;
+ return true;
+ }
+ return false;
+ }
+
+ public synchronized boolean removeState(int state) {
+ boolean result = (mCurrent & state) == state;
+ mCurrent &= (~state);
+ return result;
+ }
+
+ public synchronized int current() {
+ return mCurrent;
+ }
+ }
+
+ private static class Event {
+ public static final int PREPARE = 1;
+ public static final int BEGIN = 2;
+ public static final int STEP = 3;
+ public static final int STOP = 4;
+ public static final int PAUSE = 6;
+ public static final int HALT = 7;
+ public static final int RESUME = 8;
+ public static final int RESTART = 9;
+ public static final int FLUSH = 10;
+ public static final int TEARDOWN = 11;
+ public static final int KILL = 12;
+ public static final int RELEASE_FRAMES = 13;
+
+ public int code;
+ public Object object;
+
+ public Event(int code, Object object) {
+ this.code = code;
+ this.object = object;
+ }
+ }
+
+ private final class GraphRunLoop implements Runnable {
+
+ private State mState = new State();
+ private final boolean mAllowOpenGL;
+ private RenderTarget mRenderTarget = null;
+ private LinkedBlockingQueue mEventQueue = new LinkedBlockingQueue();
+ private Exception mCaughtException = null;
+ private boolean mClosedSuccessfully = true;
+ private Stack mFilters = new Stack();
+ private Stack mSubListeners = new Stack();
+ private Set mOpenedGraphs = new HashSet();
+ public ConditionVariable mStopCondition = new ConditionVariable(true);
+
+ private void loop() {
+ boolean killed = false;
+ while (!killed) {
+ try {
+ Event event = nextEvent();
+ if (event == null) continue;
+ switch (event.code) {
+ case Event.PREPARE:
+ onPrepare((FilterGraph)event.object);
+ break;
+ case Event.BEGIN:
+ onBegin();
+ break;
+ case Event.STEP:
+ onStep();
+ break;
+ case Event.STOP:
+ onStop();
+ break;
+ case Event.PAUSE:
+ onPause();
+ break;
+ case Event.HALT:
+ onHalt();
+ break;
+ case Event.RESUME:
+ onResume();
+ break;
+ case Event.RESTART:
+ onRestart();
+ break;
+ case Event.FLUSH:
+ onFlush();
+ break;
+ case Event.TEARDOWN:
+ onTearDown((FilterGraph)event.object);
+ break;
+ case Event.KILL:
+ killed = true;
+ break;
+ case Event.RELEASE_FRAMES:
+ onReleaseFrames();
+ break;
+ }
+ } catch (Exception e) {
+ if (mCaughtException == null) {
+ mCaughtException = e;
+ mClosedSuccessfully = true;
+ e.printStackTrace();
+ pushEvent(STOP_EVENT);
+ } else {
+ // Exception during exception recovery? Abort all processing. Do not
+ // overwrite the original exception.
+ mClosedSuccessfully = false;
+ mEventQueue.clear();
+ cleanUp();
+ }
+ }
+ }
+ }
+
+ public GraphRunLoop(boolean allowOpenGL) {
+ mAllowOpenGL = allowOpenGL;
+ }
+
+ @Override
+ public void run() {
+ onInit();
+ loop();
+ onDestroy();
+ }
+
+ public void enterSubGraph(FilterGraph graph, SubListener listener) {
+ if (mState.check(State.RUNNING)) {
+ onOpenGraph(graph);
+ mSubListeners.push(listener);
+ }
+ }
+
+ public void pushWakeEvent(Event event) {
+ // This is of course not race-condition proof. The worst case is that the event
+ // is pushed even though the queue was not empty, which is acceptible for our cases.
+ if (mEventQueue.isEmpty()) {
+ pushEvent(event);
+ }
+ }
+
+ public void pushEvent(Event event) {
+ mEventQueue.offer(event);
+ }
+
+ public void pushEvent(int eventId, Object object) {
+ mEventQueue.offer(new Event(eventId, object));
+ }
+
+ public boolean checkState(int state) {
+ return mState.check(state);
+ }
+
+ public ConditionVariable getStopCondition() {
+ return mStopCondition;
+ }
+
+ public boolean isOpenGLAllowed() {
+ // Does not need synchronization as mAllowOpenGL flag is final.
+ return mAllowOpenGL;
+ }
+
+ private Event nextEvent() {
+ try {
+ return mEventQueue.take();
+ } catch (InterruptedException e) {
+ // Ignore and keep going.
+ Log.w("GraphRunner", "Event queue processing was interrupted.");
+ return null;
+ }
+ }
+
+ private void onPause() {
+ mState.addState(State.PAUSED);
+ }
+
+ private void onResume() {
+ if (mState.removeState(State.PAUSED)) {
+ if (mState.current() == State.RUNNING) {
+ pushEvent(STEP_EVENT);
+ }
+ }
+ }
+
+ private void onHalt() {
+ if (mState.addState(State.HALTED) && mState.check(State.RUNNING)) {
+ closeAllFilters();
+ }
+ }
+
+ private void onRestart() {
+ if (mState.removeState(State.HALTED)) {
+ if (mState.current() == State.RUNNING) {
+ pushEvent(STEP_EVENT);
+ }
+ }
+ }
+
+ private void onDestroy() {
+ mFrameManager.destroyBackings();
+ if (mRenderTarget != null) {
+ mRenderTarget.release();
+ mRenderTarget = null;
+ }
+ }
+
+ private void onReleaseFrames() {
+ mFrameManager.destroyBackings();
+ }
+
+ private void onInit() {
+ mThreadRunner.set(GraphRunner.this);
+ if (getContext().isOpenGLSupported()) {
+ mRenderTarget = RenderTarget.newTarget(1, 1);
+ mRenderTarget.focus();
+ }
+ }
+
+ private void onPrepare(FilterGraph graph) {
+ if (mState.current() == State.STOPPED) {
+ mState.setState(State.PREPARING);
+ mCaughtException = null;
+ onOpenGraph(graph);
+ }
+ }
+
+ private void onOpenGraph(FilterGraph graph) {
+ loadFilters(graph);
+ mOpenedGraphs.add(graph);
+ mScheduler.prepare(currentFilters());
+ pushEvent(BEGIN_EVENT);
+ }
+
+ private void onBegin() {
+ if (mState.current() == State.PREPARING) {
+ mState.setState(State.RUNNING);
+ pushEvent(STEP_EVENT);
+ }
+ }
+
+ private void onStarve() {
+ mFilters.pop();
+ if (mFilters.empty()) {
+ onStop();
+ } else {
+ SubListener listener = mSubListeners.pop();
+ if (listener != null) {
+ listener.onSubGraphRunEnded(GraphRunner.this);
+ }
+ mScheduler.prepare(currentFilters());
+ pushEvent(STEP_EVENT);
+ }
+ }
+
+ private void onStop() {
+ if (mState.check(State.RUNNING)) {
+ // Close filters if not already halted (and already closed)
+ if (!mState.check(State.HALTED)) {
+ closeAllFilters();
+ }
+ cleanUp();
+ }
+ }
+
+ private void cleanUp() {
+ mState.setState(State.STOPPED);
+ if (flushOnClose()) {
+ onFlush();
+ }
+ mOpenedGraphs.clear();
+ mFilters.clear();
+ onRunnerStopped(mCaughtException, mClosedSuccessfully);
+ mStopCondition.open();
+ }
+
+ private void onStep() {
+ if (mState.current() == State.RUNNING) {
+ Filter bestFilter = null;
+ long maxPriority = PRIORITY_STOP;
+ mScheduler.beginStep();
+ Filter[] filters = currentFilters();
+ for (int i = 0; i < filters.length; ++i) {
+ Filter filter = filters[i];
+ long priority = mScheduler.priorityForFilter(filter);
+ if (priority > maxPriority) {
+ maxPriority = priority;
+ bestFilter = filter;
+ }
+ }
+ if (maxPriority == PRIORITY_SLEEP) {
+ // NOOP: When going into sleep mode, we simply do not schedule another node.
+ // If some other event (such as a resume()) does schedule, then we may schedule
+ // during sleeping. This is an edge case an irrelevant. (On the other hand,
+ // going into a dedicated "sleep state" requires highly complex synchronization
+ // to not "miss" a wake-up event. Thus we choose the more defensive approach
+ // here).
+ } else if (maxPriority == PRIORITY_STOP) {
+ onStarve();
+ } else {
+ scheduleFilter(bestFilter);
+ pushEvent(STEP_EVENT);
+ }
+ } else {
+ Log.w("GraphRunner", "State is not running! (" + mState.current() + ")");
+ }
+ }
+
+ private void onFlush() {
+ if (mState.check(State.HALTED) || mState.check(State.STOPPED)) {
+ for (FilterGraph graph : mOpenedGraphs) {
+ graph.flushFrames();
+ }
+ }
+ }
+
+ private void onTearDown(FilterGraph graph) {
+ for (Filter filter : graph.getAllFilters()) {
+ filter.performTearDown();
+ }
+ graph.wipe();
+ }
+
+ private void loadFilters(FilterGraph graph) {
+ Filter[] filters = graph.getAllFilters();
+ mFilters.push(filters);
+ }
+
+ private void closeAllFilters() {
+ for (FilterGraph graph : mOpenedGraphs) {
+ closeFilters(graph);
+ }
+ }
+
+ private void closeFilters(FilterGraph graph) {
+ // [Non-iterator looping]
+ Log.v("GraphRunner", "CLOSING FILTERS");
+ Filter[] filters = graph.getAllFilters();
+ boolean isVerbose = isVerbose();
+ for (int i = 0; i < filters.length; ++i) {
+ if (isVerbose) {
+ Log.i("GraphRunner", "Closing Filter " + filters[i] + "!");
+ }
+ filters[i].softReset();
+ }
+ }
+
+ private Filter[] currentFilters() {
+ return mFilters.peek();
+ }
+
+ private void scheduleFilter(Filter filter) {
+ long scheduleTime = 0;
+ if (isVerbose()) {
+ scheduleTime = SystemClock.elapsedRealtime();
+ Log.i("GraphRunner", scheduleTime + ": Scheduling " + filter + "!");
+ }
+ filter.execute();
+ if (isVerbose()) {
+ long nowTime = SystemClock.elapsedRealtime();
+ Log.i("GraphRunner",
+ "-> Schedule time (" + filter + ") = " + (nowTime - scheduleTime) + " ms.");
+ }
+ }
+
+ }
+
+ // GraphRunner.Scheduler classes ///////////////////////////////////////////////////////////////
+ private interface Scheduler {
+ public void prepare(Filter[] filters);
+
+ public int getStrategy();
+
+ public void beginStep();
+
+ public long priorityForFilter(Filter filter);
+
+ }
+
+ private class LruScheduler implements Scheduler {
+
+ private long mNow;
+
+ @Override
+ public void prepare(Filter[] filters) {
+ }
+
+ @Override
+ public int getStrategy() {
+ return STRATEGY_LRU;
+ }
+
+ @Override
+ public void beginStep() {
+ // TODO(renn): We could probably do this with a simple GraphRunner counter that would
+ // represent GraphRunner local time. This would allow us to use integers instead of
+ // longs, and save us calls to the system clock.
+ mNow = SystemClock.elapsedRealtime();
+ }
+
+ @Override
+ public long priorityForFilter(Filter filter) {
+ if (filter.isSleeping()) {
+ return PRIORITY_SLEEP;
+ } else if (filter.canSchedule()) {
+ return mNow - filter.getLastScheduleTime();
+ } else {
+ return PRIORITY_STOP;
+ }
+ }
+
+ }
+
+ private class LfuScheduler implements Scheduler {
+
+ private final int MAX_PRIORITY = Integer.MAX_VALUE;
+
+ @Override
+ public void prepare(Filter[] filters) {
+ // [Non-iterator looping]
+ for (int i = 0; i < filters.length; ++i) {
+ filters[i].resetScheduleCount();
+ }
+ }
+
+ @Override
+ public int getStrategy() {
+ return STRATEGY_LFU;
+ }
+
+ @Override
+ public void beginStep() {
+ }
+
+ @Override
+ public long priorityForFilter(Filter filter) {
+ return filter.isSleeping() ? PRIORITY_SLEEP
+ : (filter.canSchedule() ? (MAX_PRIORITY - filter.getScheduleCount())
+ : PRIORITY_STOP);
+ }
+
+ }
+
+ private class OneShotScheduler extends LfuScheduler {
+ private int mCurCount = 1;
+
+ @Override
+ public void prepare(Filter[] filters) {
+ // [Non-iterator looping]
+ for (int i = 0; i < filters.length; ++i) {
+ filters[i].resetScheduleCount();
+ }
+ }
+
+ @Override
+ public int getStrategy() {
+ return STRATEGY_ONESHOT;
+ }
+
+ @Override
+ public void beginStep() {
+ }
+
+ @Override
+ public long priorityForFilter(Filter filter) {
+ return filter.getScheduleCount() < mCurCount ? super.priorityForFilter(filter)
+ : PRIORITY_STOP;
+ }
+
+ }
+
+ // GraphRunner.Listener callback class /////////////////////////////////////////////////////////
+ public interface Listener {
+ /**
+ * Callback method that is called when the runner completes a run. This method is called
+ * only if the graph completed without an error.
+ */
+ public void onGraphRunnerStopped(GraphRunner runner);
+
+ /**
+ * Callback method that is called when runner encounters an error.
+ *
+ * Any exceptions thrown in the GraphRunner's thread will cause the run to abort. The
+ * thrown exception is passed to the listener in this method. If no listener is set, the
+ * exception message is logged to the error stream. You will not receive an
+ * {@link #onGraphRunnerStopped(GraphRunner)} callback in case of an error.
+ *
+ * @param exception the exception that was thrown.
+ * @param closedSuccessfully true, if the graph was closed successfully after the error.
+ */
+ public void onGraphRunnerError(Exception exception, boolean closedSuccessfully);
+ }
+
+ public interface SubListener {
+ public void onSubGraphRunEnded(GraphRunner runner);
+ }
+
+ /**
+ * Config class to setup a GraphRunner with a custom configuration.
+ *
+ * The configuration object is passed to the constructor. Any changes to it will not affect
+ * the created GraphRunner instance.
+ */
+ public static class Config {
+ /** The runner's thread priority. */
+ public int threadPriority = Thread.NORM_PRIORITY;
+ /** Whether to allow filters to use OpenGL or not. */
+ public boolean allowOpenGL = true;
+ }
+
+ /** Parameters shared between run-thread and GraphRunner frontend. */
+ private class RunParameters {
+ public Listener listener = null;
+ public boolean isVerbose = false;
+ public boolean flushOnClose = true;
+ }
+
+ // GraphRunner implementation //////////////////////////////////////////////////////////////////
+ /** Schedule strategy: From set of candidates, pick a random one. */
+ public static final int STRATEGY_RANDOM = 1;
+ /** Schedule strategy: From set of candidates, pick node executed least recently executed. */
+ public static final int STRATEGY_LRU = 2;
+ /** Schedule strategy: From set of candidates, pick node executed least number of times. */
+ public static final int STRATEGY_LFU = 3;
+ /** Schedule strategy: Schedules no node more than once. */
+ public static final int STRATEGY_ONESHOT = 4;
+
+ private final MffContext mContext;
+
+ private FilterGraph mRunningGraph = null;
+ private Set mGraphs = new HashSet();
+
+ private Scheduler mScheduler;
+
+ private GraphRunLoop mRunLoop;
+
+ private Thread mRunThread = null;
+
+ private FrameManager mFrameManager = null;
+
+ private static ThreadLocal mThreadRunner = new ThreadLocal();
+
+ private RunParameters mParams = new RunParameters();
+
+ /**
+ * Creates a new GraphRunner with the default configuration. You must attach FilterGraph
+ * instances to this runner before you can execute any of these graphs.
+ *
+ * @param context The MffContext instance for this runner.
+ */
+ public GraphRunner(MffContext context) {
+ mContext = context;
+ init(new Config());
+ }
+
+ /**
+ * Creates a new GraphRunner with the specified configuration. You must attach FilterGraph
+ * instances to this runner before you can execute any of these graphs.
+ *
+ * @param context The MffContext instance for this runner.
+ * @param config A Config instance with the configuration of this runner.
+ */
+ public GraphRunner(MffContext context, Config config) {
+ mContext = context;
+ init(config);
+ }
+
+ /**
+ * Returns the currently running graph-runner.
+ * @return The currently running graph-runner.
+ */
+ public static GraphRunner current() {
+ return mThreadRunner.get();
+ }
+
+ /**
+ * Returns the graph that this runner is currently executing. Returns null if no graph is
+ * currently being executed by this runner.
+ *
+ * @return the FilterGraph instance that this GraphRunner is executing.
+ */
+ public synchronized FilterGraph getRunningGraph() {
+ return mRunningGraph;
+ }
+
+ /**
+ * Returns the context that this runner is bound to.
+ *
+ * @return the MffContext instance that this runner is bound to.
+ */
+ public MffContext getContext() {
+ return mContext;
+ }
+
+ /**
+ * Begins graph execution. The graph filters are scheduled and executed until processing
+ * finishes or is stopped.
+ */
+ public synchronized void start(FilterGraph graph) {
+ if (graph.mRunner != this) {
+ throw new IllegalArgumentException("Graph must be attached to runner!");
+ }
+ mRunningGraph = graph;
+ mRunLoop.getStopCondition().close();
+ mRunLoop.pushEvent(Event.PREPARE, graph);
+ }
+
+ /**
+ * Begin executing a sub-graph. This only succeeds if the current runner is already
+ * executing.
+ */
+ public void enterSubGraph(FilterGraph graph, SubListener listener) {
+ if (Thread.currentThread() != mRunThread) {
+ throw new RuntimeException("enterSubGraph must be called from the runner's thread!");
+ }
+ mRunLoop.enterSubGraph(graph, listener);
+ }
+
+ /**
+ * Waits until graph execution has finished or stopped with an error.
+ * Care must be taken when using this method to not block the UI thread. This is typically
+ * used when a graph is run in one-shot mode to compute a result.
+ */
+ public void waitUntilStop() {
+ mRunLoop.getStopCondition().block();
+ }
+
+ /**
+ * Pauses graph execution.
+ */
+ public void pause() {
+ mRunLoop.pushEvent(PAUSE_EVENT);
+ }
+
+ /**
+ * Resumes graph execution after pausing.
+ */
+ public void resume() {
+ mRunLoop.pushEvent(RESUME_EVENT);
+ }
+
+ /**
+ * Stops graph execution.
+ */
+ public void stop() {
+ mRunLoop.pushEvent(STOP_EVENT);
+ }
+
+ /**
+ * Returns whether the graph is currently being executed. A graph is considered to be running,
+ * even if it is paused or in the process of being stopped.
+ *
+ * @return true, if the graph is currently being executed.
+ */
+ public boolean isRunning() {
+ return !mRunLoop.checkState(State.STOPPED);
+ }
+
+ /**
+ * Returns whether the graph is currently paused.
+ *
+ * @return true, if the graph is currently paused.
+ */
+ public boolean isPaused() {
+ return mRunLoop.checkState(State.PAUSED);
+ }
+
+ /**
+ * Returns whether the graph is currently stopped.
+ *
+ * @return true, if the graph is currently stopped.
+ */
+ public boolean isStopped() {
+ return mRunLoop.checkState(State.STOPPED);
+ }
+
+ /**
+ * Sets the filter scheduling strategy. This method can not be called when the GraphRunner is
+ * running.
+ *
+ * @param strategy a constant specifying which scheduler strategy to use.
+ * @throws RuntimeException if the GraphRunner is running.
+ * @throws IllegalArgumentException if invalid strategy is specified.
+ * @see #getSchedulerStrategy()
+ */
+ public void setSchedulerStrategy(int strategy) {
+ if (isRunning()) {
+ throw new RuntimeException(
+ "Attempting to change scheduling strategy on running " + "GraphRunner!");
+ }
+ createScheduler(strategy);
+ }
+
+ /**
+ * Returns the current scheduling strategy.
+ *
+ * @return the scheduling strategy used by this GraphRunner.
+ * @see #setSchedulerStrategy(int)
+ */
+ public int getSchedulerStrategy() {
+ return mScheduler.getStrategy();
+ }
+
+ /**
+ * Set whether or not the runner is verbose. When set to true, the runner will output individual
+ * scheduling steps that may help identify and debug problems in the graph structure. The
+ * default is false.
+ *
+ * @param isVerbose true, if the GraphRunner should log scheduling details.
+ * @see #isVerbose()
+ */
+ public void setIsVerbose(boolean isVerbose) {
+ synchronized (mParams) {
+ mParams.isVerbose = isVerbose;
+ }
+ }
+
+ /**
+ * Returns whether the GraphRunner is verbose.
+ *
+ * @return true, if the GraphRunner logs scheduling details.
+ * @see #setIsVerbose(boolean)
+ */
+ public boolean isVerbose() {
+ synchronized (mParams) {
+ return mParams.isVerbose;
+ }
+ }
+
+ /**
+ * Returns whether Filters of this GraphRunner can use OpenGL.
+ *
+ * Filters may use OpenGL if the MffContext supports OpenGL and the GraphRunner allows it.
+ *
+ * @return true, if Filters are allowed to use OpenGL.
+ */
+ public boolean isOpenGLSupported() {
+ return mRunLoop.isOpenGLAllowed() && mContext.isOpenGLSupported();
+ }
+
+ /**
+ * Enable flushing all frames from the graph when running completes.
+ *
+ * If this is set to false, then frames may remain in the pipeline even after running completes.
+ * The default value is true.
+ *
+ * @param flush true, if the GraphRunner should flush the graph when running completes.
+ * @see #flushOnClose()
+ */
+ public void setFlushOnClose(boolean flush) {
+ synchronized (mParams) {
+ mParams.flushOnClose = flush;
+ }
+ }
+
+ /**
+ * Returns whether the GraphRunner flushes frames when running completes.
+ *
+ * @return true, if the GraphRunner flushes frames when running completes.
+ * @see #setFlushOnClose(boolean)
+ */
+ public boolean flushOnClose() {
+ synchronized (mParams) {
+ return mParams.flushOnClose;
+ }
+ }
+
+ /**
+ * Sets the listener for receiving runtime events. A GraphRunner.Listener instance can be used
+ * to determine when certain events occur during graph execution (and react on them). See the
+ * {@link GraphRunner.Listener} class for details.
+ *
+ * @param listener the GraphRunner.Listener instance to set.
+ * @see #getListener()
+ */
+ public void setListener(Listener listener) {
+ synchronized (mParams) {
+ mParams.listener = listener;
+ }
+ }
+
+ /**
+ * Returns the currently assigned GraphRunner.Listener.
+ *
+ * @return the currently assigned GraphRunner.Listener instance.
+ * @see #setListener(Listener)
+ */
+ public Listener getListener() {
+ synchronized (mParams) {
+ return mParams.listener;
+ }
+ }
+
+ /**
+ * Returns the FrameManager that manages the runner's frames.
+ *
+ * @return the FrameManager instance that manages the runner's frames.
+ */
+ public FrameManager getFrameManager() {
+ return mFrameManager;
+ }
+
+ /**
+ * Tear down a GraphRunner and all its resources.
+ *
+ * You must make sure that before calling this, no more graphs are attached to this runner.
+ * Typically, graphs are removed from runners when they are torn down.
+ *
+ * @throws IllegalStateException if there are still graphs attached to this runner.
+ */
+ public void tearDown() {
+ synchronized (mGraphs) {
+ if (!mGraphs.isEmpty()) {
+ throw new IllegalStateException("Attempting to tear down runner with "
+ + mGraphs.size() + " graphs still attached!");
+ }
+ }
+ mRunLoop.pushEvent(KILL_EVENT);
+ // Wait for thread to complete, so that everything is torn down by the time we return.
+ try {
+ mRunThread.join();
+ } catch (InterruptedException e) {
+ Log.e("GraphRunner", "Error waiting for runner thread to finish!");
+ }
+ }
+
+ /**
+ * Release all frames managed by this runner.
+ *
+ * Note, that you must make sure no graphs are attached to this runner before calling this
+ * method, as otherwise Filters in the graph may reference frames that are now released.
+ *
+ * TODO: Eventually, this method should be removed. Instead we should have better analysis
+ * that catches leaking frames from filters.
+ *
+ * @throws IllegalStateException if there are still graphs attached to this runner.
+ */
+ public void releaseFrames() {
+ synchronized (mGraphs) {
+ if (!mGraphs.isEmpty()) {
+ throw new IllegalStateException("Attempting to release frames with "
+ + mGraphs.size() + " graphs still attached!");
+ }
+ }
+ mRunLoop.pushEvent(RELEASE_FRAMES_EVENT);
+ }
+
+ // Core internal methods ///////////////////////////////////////////////////////////////////////
+ void attachGraph(FilterGraph graph) {
+ synchronized (mGraphs) {
+ mGraphs.add(graph);
+ }
+ }
+
+ void signalWakeUp() {
+ mRunLoop.pushWakeEvent(STEP_EVENT);
+ }
+
+ void begin() {
+ mRunLoop.pushEvent(BEGIN_EVENT);
+ }
+
+ /** Like pause(), but closes all filters. Can be resumed using restart(). */
+ void halt() {
+ mRunLoop.pushEvent(HALT_EVENT);
+ }
+
+ /** Resumes a previously halted runner, and restores it to its non-halted state. */
+ void restart() {
+ mRunLoop.pushEvent(RESTART_EVENT);
+ }
+
+ /**
+ * Tears down the specified graph.
+ *
+ * The graph must be attached to this runner.
+ */
+ void tearDownGraph(FilterGraph graph) {
+ if (graph.getRunner() != this) {
+ throw new IllegalArgumentException("Attempting to tear down graph with foreign "
+ + "GraphRunner!");
+ }
+ mRunLoop.pushEvent(Event.TEARDOWN, graph);
+ synchronized (mGraphs) {
+ mGraphs.remove(graph);
+ }
+ }
+
+ /**
+ * Remove all frames that are waiting to be processed.
+ *
+ * Removes and releases frames that are waiting in the graph connections of the currently
+ * halted graphs, i.e. frames that are waiting to be processed. This does not include frames
+ * that may be held or cached by filters themselves.
+ *
+ * TODO: With the new sub-graph architecture, this can now be simplified and made public.
+ * It can then no longer rely on opened graphs, and instead flush a graph and all its
+ * sub-graphs.
+ */
+ void flushFrames() {
+ mRunLoop.pushEvent(FLUSH_EVENT);
+ }
+
+ // Private methods /////////////////////////////////////////////////////////////////////////////
+ private void init(Config config) {
+ mFrameManager = new FrameManager(this, FrameManager.FRAME_CACHE_LRU);
+ createScheduler(STRATEGY_LRU);
+ mRunLoop = new GraphRunLoop(config.allowOpenGL);
+ mRunThread = new Thread(mRunLoop);
+ mRunThread.setPriority(config.threadPriority);
+ mRunThread.start();
+ mContext.addRunner(this);
+ }
+
+ private void createScheduler(int strategy) {
+ switch (strategy) {
+ case STRATEGY_LRU:
+ mScheduler = new LruScheduler();
+ break;
+ case STRATEGY_LFU:
+ mScheduler = new LfuScheduler();
+ break;
+ case STRATEGY_ONESHOT:
+ mScheduler = new OneShotScheduler();
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown schedule-strategy constant " + strategy + "!");
+ }
+ }
+
+ // Called within the runner's thread
+ private void onRunnerStopped(final Exception exception, final boolean closed) {
+ mRunningGraph = null;
+ synchronized (mParams) {
+ if (mParams.listener != null) {
+ getContext().postRunnable(new Runnable() {
+ @Override
+ public void run() {
+ if (exception == null) {
+ mParams.listener.onGraphRunnerStopped(GraphRunner.this);
+ } else {
+ mParams.listener.onGraphRunnerError(exception, closed);
+ }
+ }
+ });
+ } else if (exception != null) {
+ Log.e("GraphRunner",
+ "Uncaught exception during graph execution! Stack Trace: ");
+ exception.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ImageShader.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ImageShader.java
new file mode 100644
index 0000000000000..0ec50a392c451
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ImageShader.java
@@ -0,0 +1,793 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.graphics.RectF;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import androidx.media.filterfw.geometry.Quad;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * Convenience class to perform GL shader operations on image data.
+ *
+ * The ImageShader class greatly simplifies the task of running GL shader language kernels over
+ * Frame data buffers that contain RGBA image data.
+ *
+ * TODO: More documentation
+ *
+ */
+public class ImageShader {
+
+ private int mProgram = 0;
+ private boolean mClearsOutput = false;
+ private float[] mClearColor = { 0f, 0f, 0f, 0f };
+ private boolean mBlendEnabled = false;
+ private int mSFactor = GLES20.GL_SRC_ALPHA;
+ private int mDFactor = GLES20.GL_ONE_MINUS_SRC_ALPHA;
+ private int mDrawMode = GLES20.GL_TRIANGLE_STRIP;
+ private int mVertexCount = 4;
+ private int mBaseTexUnit = GLES20.GL_TEXTURE0;
+ private int mClearBuffers = GLES20.GL_COLOR_BUFFER_BIT;
+ private float[] mSourceCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f };
+ private float[] mTargetCoords = new float[] { -1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f };
+
+ private HashMap mUniforms;
+ private HashMap mAttributes = new HashMap();
+
+ private final static int FLOAT_SIZE = 4;
+
+ private final static String mDefaultVertexShader =
+ "attribute vec4 a_position;\n" +
+ "attribute vec2 a_texcoord;\n" +
+ "varying vec2 v_texcoord;\n" +
+ "void main() {\n" +
+ " gl_Position = a_position;\n" +
+ " v_texcoord = a_texcoord;\n" +
+ "}\n";
+
+ private final static String mIdentityShader =
+ "precision mediump float;\n" +
+ "uniform sampler2D tex_sampler_0;\n" +
+ "varying vec2 v_texcoord;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(tex_sampler_0, v_texcoord);\n" +
+ "}\n";
+
+ private static class VertexAttribute {
+ private String mName;
+ private boolean mIsConst;
+ private int mIndex;
+ private boolean mShouldNormalize;
+ private int mOffset;
+ private int mStride;
+ private int mComponents;
+ private int mType;
+ private int mVbo;
+ private int mLength;
+ private FloatBuffer mValues;
+
+ public VertexAttribute(String name, int index) {
+ mName = name;
+ mIndex = index;
+ mLength = -1;
+ }
+
+ public void set(boolean normalize, int stride, int components, int type, float[] values) {
+ mIsConst = false;
+ mShouldNormalize = normalize;
+ mStride = stride;
+ mComponents = components;
+ mType = type;
+ mVbo = 0;
+ if (mLength != values.length){
+ initBuffer(values);
+ mLength = values.length;
+ }
+ copyValues(values);
+ }
+
+ public void set(boolean normalize, int offset, int stride, int components, int type,
+ int vbo){
+ mIsConst = false;
+ mShouldNormalize = normalize;
+ mOffset = offset;
+ mStride = stride;
+ mComponents = components;
+ mType = type;
+ mVbo = vbo;
+ mValues = null;
+ }
+
+ public boolean push() {
+ if (mIsConst) {
+ switch (mComponents) {
+ case 1:
+ GLES20.glVertexAttrib1fv(mIndex, mValues);
+ break;
+ case 2:
+ GLES20.glVertexAttrib2fv(mIndex, mValues);
+ break;
+ case 3:
+ GLES20.glVertexAttrib3fv(mIndex, mValues);
+ break;
+ case 4:
+ GLES20.glVertexAttrib4fv(mIndex, mValues);
+ break;
+ default:
+ return false;
+ }
+ GLES20.glDisableVertexAttribArray(mIndex);
+ } else {
+ if (mValues != null) {
+ // Note that we cannot do any size checking here, as the correct component
+ // count depends on the drawing step. GL should catch such errors then, and
+ // we will report them to the user.
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+ GLES20.glVertexAttribPointer(mIndex,
+ mComponents,
+ mType,
+ mShouldNormalize,
+ mStride,
+ mValues);
+ } else {
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVbo);
+ GLES20.glVertexAttribPointer(mIndex,
+ mComponents,
+ mType,
+ mShouldNormalize,
+ mStride,
+ mOffset);
+ }
+ GLES20.glEnableVertexAttribArray(mIndex);
+ }
+ GLToolbox.checkGlError("Set vertex-attribute values");
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+
+ private void initBuffer(float[] values) {
+ mValues = ByteBuffer.allocateDirect(values.length * FLOAT_SIZE)
+ .order(ByteOrder.nativeOrder()).asFloatBuffer();
+ }
+
+ private void copyValues(float[] values) {
+ mValues.put(values).position(0);
+ }
+
+ }
+
+ private static final class ProgramUniform {
+ private String mName;
+ private int mLocation;
+ private int mType;
+ private int mSize;
+
+ public ProgramUniform(int program, int index) {
+ int[] len = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, len, 0);
+
+ int[] type = new int[1];
+ int[] size = new int[1];
+ byte[] name = new byte[len[0]];
+ int[] ignore = new int[1];
+
+ GLES20.glGetActiveUniform(program, index, len[0], ignore, 0, size, 0, type, 0, name, 0);
+ mName = new String(name, 0, strlen(name));
+ mLocation = GLES20.glGetUniformLocation(program, mName);
+ mType = type[0];
+ mSize = size[0];
+ GLToolbox.checkGlError("Initializing uniform");
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public int getLocation() {
+ return mLocation;
+ }
+
+ public int getSize() {
+ return mSize;
+ }
+ }
+
+ public ImageShader(String fragmentShader) {
+ mProgram = createProgram(mDefaultVertexShader, fragmentShader);
+ scanUniforms();
+ }
+
+ public ImageShader(String vertexShader, String fragmentShader) {
+ mProgram = createProgram(vertexShader, fragmentShader);
+ scanUniforms();
+ }
+
+ public static ImageShader createIdentity() {
+ return new ImageShader(mIdentityShader);
+ }
+
+ public static ImageShader createIdentity(String vertexShader) {
+ return new ImageShader(vertexShader, mIdentityShader);
+ }
+
+ public static void renderTextureToTarget(TextureSource texture,
+ RenderTarget target,
+ int width,
+ int height) {
+ ImageShader shader = RenderTarget.currentTarget().getIdentityShader();
+ shader.process(texture, target, width, height);
+ }
+
+ public void process(FrameImage2D input, FrameImage2D output) {
+ TextureSource texSource = input.lockTextureSource();
+ RenderTarget renderTarget = output.lockRenderTarget();
+ processMulti(new TextureSource[] { texSource },
+ renderTarget,
+ output.getWidth(),
+ output.getHeight());
+ input.unlock();
+ output.unlock();
+ }
+
+ public void processMulti(FrameImage2D[] inputs, FrameImage2D output) {
+ TextureSource[] texSources = new TextureSource[inputs.length];
+ for (int i = 0; i < inputs.length; ++i) {
+ texSources[i] = inputs[i].lockTextureSource();
+ }
+ RenderTarget renderTarget = output.lockRenderTarget();
+ processMulti(texSources,
+ renderTarget,
+ output.getWidth(),
+ output.getHeight());
+ for (FrameImage2D input : inputs) {
+ input.unlock();
+ }
+ output.unlock();
+ }
+
+ public void process(TextureSource texture, RenderTarget target, int width, int height) {
+ processMulti(new TextureSource[] { texture }, target, width, height);
+ }
+
+ public void processMulti(TextureSource[] sources, RenderTarget target, int width, int height) {
+ GLToolbox.checkGlError("Unknown Operation");
+ checkExecutable();
+ checkTexCount(sources.length);
+ focusTarget(target, width, height);
+ pushShaderState();
+ bindInputTextures(sources);
+ render();
+ }
+
+ public void processNoInput(FrameImage2D output) {
+ RenderTarget renderTarget = output.lockRenderTarget();
+ processNoInput(renderTarget, output.getWidth(), output.getHeight());
+ output.unlock();
+ }
+
+ public void processNoInput(RenderTarget target, int width, int height) {
+ processMulti(new TextureSource[] {}, target, width, height);
+ }
+
+ public int getUniformLocation(String name) {
+ return getProgramUniform(name, true).getLocation();
+ }
+
+ public int getAttributeLocation(String name) {
+ if (name.equals(positionAttributeName()) || name.equals(texCoordAttributeName())) {
+ Log.w("ImageShader", "Attempting to access internal attribute '" + name
+ + "' directly!");
+ }
+ int loc = GLES20.glGetAttribLocation(mProgram, name);
+ if (loc < 0) {
+ throw new RuntimeException("Unknown attribute '" + name + "' in shader program!");
+ }
+ return loc;
+ }
+
+ public void setUniformValue(String uniformName, int value) {
+ useProgram();
+ int uniformHandle = getUniformLocation(uniformName);
+ GLES20.glUniform1i(uniformHandle, value);
+ GLToolbox.checkGlError("Set uniform value (" + uniformName + ")");
+ }
+
+ public void setUniformValue(String uniformName, float value) {
+ useProgram();
+ int uniformHandle = getUniformLocation(uniformName);
+ GLES20.glUniform1f(uniformHandle, value);
+ GLToolbox.checkGlError("Set uniform value (" + uniformName + ")");
+ }
+
+ public void setUniformValue(String uniformName, int[] values) {
+ ProgramUniform uniform = getProgramUniform(uniformName, true);
+ useProgram();
+ int len = values.length;
+ switch (uniform.getType()) {
+ case GLES20.GL_INT:
+ checkUniformAssignment(uniform, len, 1);
+ GLES20.glUniform1iv(uniform.getLocation(), len, values, 0);
+ break;
+ case GLES20.GL_INT_VEC2:
+ checkUniformAssignment(uniform, len, 2);
+ GLES20.glUniform2iv(uniform.getLocation(), len / 2, values, 0);
+ break;
+ case GLES20.GL_INT_VEC3:
+ checkUniformAssignment(uniform, len, 3);
+ GLES20.glUniform2iv(uniform.getLocation(), len / 3, values, 0);
+ break;
+ case GLES20.GL_INT_VEC4:
+ checkUniformAssignment(uniform, len, 4);
+ GLES20.glUniform2iv(uniform.getLocation(), len / 4, values, 0);
+ break;
+ default:
+ throw new RuntimeException("Cannot assign int-array to incompatible uniform type "
+ + "for uniform '" + uniformName + "'!");
+ }
+ GLToolbox.checkGlError("Set uniform value (" + uniformName + ")");
+ }
+
+
+ public void setUniformValue(String uniformName, float[] values) {
+ ProgramUniform uniform = getProgramUniform(uniformName, true);
+ useProgram();
+ int len = values.length;
+ switch (uniform.getType()) {
+ case GLES20.GL_FLOAT:
+ checkUniformAssignment(uniform, len, 1);
+ GLES20.glUniform1fv(uniform.getLocation(), len, values, 0);
+ break;
+ case GLES20.GL_FLOAT_VEC2:
+ checkUniformAssignment(uniform, len, 2);
+ GLES20.glUniform2fv(uniform.getLocation(), len / 2, values, 0);
+ break;
+ case GLES20.GL_FLOAT_VEC3:
+ checkUniformAssignment(uniform, len, 3);
+ GLES20.glUniform3fv(uniform.getLocation(), len / 3, values, 0);
+ break;
+ case GLES20.GL_FLOAT_VEC4:
+ checkUniformAssignment(uniform, len, 4);
+ GLES20.glUniform4fv(uniform.getLocation(), len / 4, values, 0);
+ break;
+ case GLES20.GL_FLOAT_MAT2:
+ checkUniformAssignment(uniform, len, 4);
+ GLES20.glUniformMatrix2fv(uniform.getLocation(), len / 4, false, values, 0);
+ break;
+ case GLES20.GL_FLOAT_MAT3:
+ checkUniformAssignment(uniform, len, 9);
+ GLES20.glUniformMatrix3fv(uniform.getLocation(), len / 9, false, values, 0);
+ break;
+ case GLES20.GL_FLOAT_MAT4:
+ checkUniformAssignment(uniform, len, 16);
+ GLES20.glUniformMatrix4fv(uniform.getLocation(), len / 16, false, values, 0);
+ break;
+ default:
+ throw new RuntimeException("Cannot assign float-array to incompatible uniform type "
+ + "for uniform '" + uniformName + "'!");
+ }
+ GLToolbox.checkGlError("Set uniform value (" + uniformName + ")");
+ }
+
+ public void setAttributeValues(String attributeName, float[] data, int components) {
+ VertexAttribute attr = getProgramAttribute(attributeName, true);
+ attr.set(false, FLOAT_SIZE * components, components, GLES20.GL_FLOAT, data);
+ }
+
+ public void setAttributeValues(String attributeName, int vbo, int type, int components,
+ int stride, int offset, boolean normalize) {
+ VertexAttribute attr = getProgramAttribute(attributeName, true);
+ attr.set(normalize, offset, stride, components, type, vbo);
+ }
+
+ public void setSourceRect(float x, float y, float width, float height) {
+ setSourceCoords(new float[] { x, y, x + width, y, x, y + height, x + width, y + height });
+ }
+
+ public void setSourceRect(RectF rect) {
+ setSourceRect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
+ }
+
+ public void setSourceQuad(Quad quad) {
+ setSourceCoords(new float[] { quad.topLeft().x, quad.topLeft().y,
+ quad.topRight().x, quad.topRight().y,
+ quad.bottomLeft().x, quad.bottomLeft().y,
+ quad.bottomRight().x, quad.bottomRight().y });
+ }
+
+ public void setSourceCoords(float[] coords) {
+ if (coords.length != 8) {
+ throw new IllegalArgumentException("Expected 8 coordinates as source coordinates but "
+ + "got " + coords.length + " coordinates!");
+ }
+ mSourceCoords = Arrays.copyOf(coords, 8);
+ }
+
+ public void setSourceTransform(float[] matrix) {
+ if (matrix.length != 16) {
+ throw new IllegalArgumentException("Expected 4x4 matrix for source transform!");
+ }
+ setSourceCoords(new float[] {
+ matrix[12],
+ matrix[13],
+
+ matrix[0] + matrix[12],
+ matrix[1] + matrix[13],
+
+ matrix[4] + matrix[12],
+ matrix[5] + matrix[13],
+
+ matrix[0] + matrix[4] + matrix[12],
+ matrix[1] + matrix[5] + matrix[13],
+ });
+ }
+
+ public void setTargetRect(float x, float y, float width, float height) {
+ setTargetCoords(new float[] { x, y,
+ x + width, y,
+ x, y + height,
+ x + width, y + height });
+ }
+
+ public void setTargetRect(RectF rect) {
+ setTargetCoords(new float[] { rect.left, rect.top,
+ rect.right, rect.top,
+ rect.left, rect.bottom,
+ rect.right, rect.bottom });
+ }
+
+ public void setTargetQuad(Quad quad) {
+ setTargetCoords(new float[] { quad.topLeft().x, quad.topLeft().y,
+ quad.topRight().x, quad.topRight().y,
+ quad.bottomLeft().x, quad.bottomLeft().y,
+ quad.bottomRight().x, quad.bottomRight().y });
+ }
+
+ public void setTargetCoords(float[] coords) {
+ if (coords.length != 8) {
+ throw new IllegalArgumentException("Expected 8 coordinates as target coordinates but "
+ + "got " + coords.length + " coordinates!");
+ }
+ mTargetCoords = new float[8];
+ for (int i = 0; i < 8; ++i) {
+ mTargetCoords[i] = coords[i] * 2f - 1f;
+ }
+ }
+
+ public void setTargetTransform(float[] matrix) {
+ if (matrix.length != 16) {
+ throw new IllegalArgumentException("Expected 4x4 matrix for target transform!");
+ }
+ setTargetCoords(new float[] {
+ matrix[12],
+ matrix[13],
+
+ matrix[0] + matrix[12],
+ matrix[1] + matrix[13],
+
+ matrix[4] + matrix[12],
+ matrix[5] + matrix[13],
+
+ matrix[0] + matrix[4] + matrix[12],
+ matrix[1] + matrix[5] + matrix[13],
+ });
+ }
+
+ public void setClearsOutput(boolean clears) {
+ mClearsOutput = clears;
+ }
+
+ public boolean getClearsOutput() {
+ return mClearsOutput;
+ }
+
+ public void setClearColor(float[] rgba) {
+ mClearColor = rgba;
+ }
+
+ public float[] getClearColor() {
+ return mClearColor;
+ }
+
+ public void setClearBufferMask(int bufferMask) {
+ mClearBuffers = bufferMask;
+ }
+
+ public int getClearBufferMask() {
+ return mClearBuffers;
+ }
+
+ public void setBlendEnabled(boolean enable) {
+ mBlendEnabled = enable;
+ }
+
+ public boolean getBlendEnabled() {
+ return mBlendEnabled;
+ }
+
+ public void setBlendFunc(int sFactor, int dFactor) {
+ mSFactor = sFactor;
+ mDFactor = dFactor;
+ }
+
+ public void setDrawMode(int drawMode) {
+ mDrawMode = drawMode;
+ }
+
+ public int getDrawMode() {
+ return mDrawMode;
+ }
+
+ public void setVertexCount(int count) {
+ mVertexCount = count;
+ }
+
+ public int getVertexCount() {
+ return mVertexCount;
+ }
+
+ public void setBaseTextureUnit(int baseTexUnit) {
+ mBaseTexUnit = baseTexUnit;
+ }
+
+ public int baseTextureUnit() {
+ return mBaseTexUnit;
+ }
+
+ public String texCoordAttributeName() {
+ return "a_texcoord";
+ }
+
+ public String positionAttributeName() {
+ return "a_position";
+ }
+
+ public String inputTextureUniformName(int index) {
+ return "tex_sampler_" + index;
+ }
+
+ public static int maxTextureUnits() {
+ return GLES20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ GLES20.glDeleteProgram(mProgram);
+ }
+
+ protected void pushShaderState() {
+ useProgram();
+ updateSourceCoordAttribute();
+ updateTargetCoordAttribute();
+ pushAttributes();
+ if (mClearsOutput) {
+ GLES20.glClearColor(mClearColor[0], mClearColor[1], mClearColor[2], mClearColor[3]);
+ GLES20.glClear(mClearBuffers);
+ }
+ if (mBlendEnabled) {
+ GLES20.glEnable(GLES20.GL_BLEND);
+ GLES20.glBlendFunc(mSFactor, mDFactor);
+ } else {
+ GLES20.glDisable(GLES20.GL_BLEND);
+ }
+ GLToolbox.checkGlError("Set render variables");
+ }
+
+ private void focusTarget(RenderTarget target, int width, int height) {
+ target.focus();
+ GLES20.glViewport(0, 0, width, height);
+ GLToolbox.checkGlError("glViewport");
+ }
+
+ private void bindInputTextures(TextureSource[] sources) {
+ for (int i = 0; i < sources.length; ++i) {
+ // Activate texture unit i
+ GLES20.glActiveTexture(baseTextureUnit() + i);
+
+ // Bind texture
+ sources[i].bind();
+
+ // Assign the texture uniform in the shader to unit i
+ int texUniform = GLES20.glGetUniformLocation(mProgram, inputTextureUniformName(i));
+ if (texUniform >= 0) {
+ GLES20.glUniform1i(texUniform, i);
+ } else {
+ throw new RuntimeException("Shader does not seem to support " + sources.length
+ + " number of input textures! Missing uniform " + inputTextureUniformName(i)
+ + "!");
+ }
+ GLToolbox.checkGlError("Binding input texture " + i);
+ }
+ }
+
+ private void pushAttributes() {
+ for (VertexAttribute attr : mAttributes.values()) {
+ if (!attr.push()) {
+ throw new RuntimeException("Unable to assign attribute value '" + attr + "'!");
+ }
+ }
+ GLToolbox.checkGlError("Push Attributes");
+ }
+
+ private void updateSourceCoordAttribute() {
+ // If attribute does not exist, simply do nothing (may be custom shader).
+ VertexAttribute attr = getProgramAttribute(texCoordAttributeName(), false);
+ // A non-null value of mSourceCoords indicates new values to be set.
+ if (mSourceCoords != null && attr != null) {
+ // Upload new source coordinates to GPU
+ attr.set(false, FLOAT_SIZE * 2, 2, GLES20.GL_FLOAT, mSourceCoords);
+ }
+ // Do not set again (even if failed, to not cause endless attempts)
+ mSourceCoords = null;
+ }
+
+ private void updateTargetCoordAttribute() {
+ // If attribute does not exist, simply do nothing (may be custom shader).
+ VertexAttribute attr = getProgramAttribute(positionAttributeName(), false);
+ // A non-null value of mTargetCoords indicates new values to be set.
+ if (mTargetCoords != null && attr != null) {
+ // Upload new target coordinates to GPU
+ attr.set(false, FLOAT_SIZE * 2, 2, GLES20.GL_FLOAT, mTargetCoords);
+ }
+ // Do not set again (even if failed, to not cause endless attempts)
+ mTargetCoords = null;
+ }
+
+ private void render() {
+ GLES20.glDrawArrays(mDrawMode, 0, mVertexCount);
+ GLToolbox.checkGlError("glDrawArrays");
+ }
+
+ private void checkExecutable() {
+ if (mProgram == 0) {
+ throw new RuntimeException("Attempting to execute invalid shader-program!");
+ }
+ }
+
+ private void useProgram() {
+ GLES20.glUseProgram(mProgram);
+ GLToolbox.checkGlError("glUseProgram");
+ }
+
+ private static void checkTexCount(int count) {
+ if (count > maxTextureUnits()) {
+ throw new RuntimeException("Number of textures passed (" + count + ") exceeds the "
+ + "maximum number of allowed texture units (" + maxTextureUnits() + ")!");
+ }
+ }
+
+ private static int loadShader(int shaderType, String source) {
+ int shader = GLES20.glCreateShader(shaderType);
+ if (shader != 0) {
+ GLES20.glShaderSource(shader, source);
+ GLES20.glCompileShader(shader);
+ int[] compiled = new int[1];
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
+ if (compiled[0] == 0) {
+ String info = GLES20.glGetShaderInfoLog(shader);
+ GLES20.glDeleteShader(shader);
+ shader = 0;
+ throw new RuntimeException("Could not compile shader " + shaderType + ":" + info);
+ }
+ }
+ return shader;
+ }
+
+ private static int createProgram(String vertexSource, String fragmentSource) {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
+ if (vertexShader == 0) {
+ throw new RuntimeException("Could not create shader-program as vertex shader "
+ + "could not be compiled!");
+ }
+ int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
+ if (pixelShader == 0) {
+ throw new RuntimeException("Could not create shader-program as fragment shader "
+ + "could not be compiled!");
+ }
+
+ int program = GLES20.glCreateProgram();
+ if (program != 0) {
+ GLES20.glAttachShader(program, vertexShader);
+ GLToolbox.checkGlError("glAttachShader");
+ GLES20.glAttachShader(program, pixelShader);
+ GLToolbox.checkGlError("glAttachShader");
+ GLES20.glLinkProgram(program);
+ int[] linkStatus = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
+ if (linkStatus[0] != GLES20.GL_TRUE) {
+ String info = GLES20.glGetProgramInfoLog(program);
+ GLES20.glDeleteProgram(program);
+ program = 0;
+ throw new RuntimeException("Could not link program: " + info);
+ }
+ }
+
+ GLES20.glDeleteShader(vertexShader);
+ GLES20.glDeleteShader(pixelShader);
+
+ return program;
+ }
+
+ private void scanUniforms() {
+ int uniformCount[] = new int [1];
+ GLES20.glGetProgramiv(mProgram, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0);
+ if (uniformCount[0] > 0) {
+ mUniforms = new HashMap(uniformCount[0]);
+ for (int i = 0; i < uniformCount[0]; ++i) {
+ ProgramUniform uniform = new ProgramUniform(mProgram, i);
+ mUniforms.put(uniform.getName(), uniform);
+ }
+ }
+ }
+
+ private ProgramUniform getProgramUniform(String name, boolean required) {
+ ProgramUniform result = mUniforms.get(name);
+ if (result == null && required) {
+ throw new IllegalArgumentException("Unknown uniform '" + name + "'!");
+ }
+ return result;
+ }
+
+ private VertexAttribute getProgramAttribute(String name, boolean required) {
+ VertexAttribute result = mAttributes.get(name);
+ if (result == null) {
+ int handle = GLES20.glGetAttribLocation(mProgram, name);
+ if (handle >= 0) {
+ result = new VertexAttribute(name, handle);
+ mAttributes.put(name, result);
+ } else if (required) {
+ throw new IllegalArgumentException("Unknown attribute '" + name + "'!");
+ }
+ }
+ return result;
+ }
+
+ private void checkUniformAssignment(ProgramUniform uniform, int values, int components) {
+ if (values % components != 0) {
+ throw new RuntimeException("Size mismatch: Attempting to assign values of size "
+ + values + " to uniform '" + uniform.getName() + "' (must be multiple of "
+ + components + ")!");
+ } else if (uniform.getSize() != values / components) {
+ throw new RuntimeException("Size mismatch: Cannot assign " + values + " values to "
+ + "uniform '" + uniform.getName() + "'!");
+ }
+ }
+
+ private static int strlen(byte[] strVal) {
+ for (int i = 0; i < strVal.length; ++i) {
+ if (strVal[i] == '\0') {
+ return i;
+ }
+ }
+ return strVal.length;
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/InputPort.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/InputPort.java
new file mode 100644
index 0000000000000..82749c51a5915
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/InputPort.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import java.lang.reflect.Field;
+
+/**
+ * Input ports are the receiving ports of frames in a filter.
+ *
+ * InputPort instances receive Frame data from connected OutputPort instances of a previous filter.
+ * Frames flow from output ports to input ports. Filters can process frame data by calling
+ * {@link #pullFrame()} on an input port. If the input port is set to wait for an input frame
+ * (see {@link #setWaitsForFrame(boolean)}), there is guaranteed to be Frame on the port before
+ * {@code onProcess()} is called. This is the default setting. Otherwise, calling
+ * {@link #pullFrame()} may return a value of {@code null}.
+ *
+ * InputPorts may be bound to fields of the Filter. When an input port is bound to a field, Frame
+ * values will be assigned to the field once a Frame is received on that port. The Frame value must
+ * be of a type that is compatible with the field type.
+ *
+ */
+public final class InputPort {
+
+ private Filter mFilter;
+ private String mName;
+ private Signature.PortInfo mInfo;
+ private FrameListener mListener = null;
+ private FrameQueue.Builder mQueueBuilder = null;
+ private FrameQueue mQueue = null;
+ private boolean mWaitForFrame = true;
+ private boolean mAutoPullEnabled = false;
+
+ public interface FrameListener {
+ public void onFrameReceived(InputPort port, Frame frame);
+ }
+
+ private class FieldBinding implements FrameListener {
+ private Field mField;
+
+ public FieldBinding(Field field) {
+ mField = field;
+ }
+
+ @Override
+ public void onFrameReceived(InputPort port, Frame frame) {
+ try {
+ if(port.mInfo.type.getNumberOfDimensions() > 0) {
+ FrameValues frameValues = frame.asFrameValues();
+ mField.set(mFilter, frameValues.getValues());
+ } else {
+ FrameValue frameValue = frame.asFrameValue();
+ mField.set(mFilter, frameValue.getValue());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Assigning frame " + frame + " to field "
+ + mField + " of filter " + mFilter + " caused exception!", e);
+ }
+ }
+ }
+
+ /**
+ * Attach this input port to an output port for frame passing.
+ *
+ * Use this method whenever you plan on passing a Frame through from an input port to an
+ * output port. This must be called from inside
+ * {@link Filter#onInputPortAttached(InputPort) onInputPortAttached}.
+ *
+ * @param outputPort the output port that Frames will be pushed to.
+ */
+ public void attachToOutputPort(OutputPort outputPort) {
+ assertInAttachmentStage();
+ mFilter.openOutputPort(outputPort);
+ mQueueBuilder.attachQueue(outputPort.getQueue());
+ }
+
+ /**
+ * Bind this input port to the specified listener.
+ *
+ * Use this when you wish to be notified of incoming frames. The listener method
+ * {@link FrameListener#onFrameReceived(InputPort, Frame)} will be called once a Frame is pulled
+ * on this port. Typically this is called from inside
+ * {@link Filter#onInputPortAttached(InputPort) onInputPortAttached}, and used in
+ * conjunction with {@link #setAutoPullEnabled(boolean)}. Overrides any previous bindings.
+ *
+ * @param listener the listener to handle incoming Frames.
+ */
+ public void bindToListener(FrameListener listener) {
+ assertInAttachmentStage();
+ mListener = listener;
+ }
+
+ /**
+ * Bind this input port to the specified field.
+ *
+ * Use this when you wish to pull frames directly into a field of the filter. This requires
+ * that the input frames can be interpreted as object-based frames of the field's class.
+ * Overrides any previous bindings.
+ *
+ * This is typically called from inside
+ * {@link Filter#onInputPortAttached(InputPort) onInputPortAttached}, and used in
+ * conjunction with {@link #setAutoPullEnabled(boolean)}.
+ *
+ * @param field the field to pull frame data into.
+ * @see #bindToFieldNamed(String)
+ * @see #setAutoPullEnabled(boolean)
+ */
+ public void bindToField(Field field) {
+ assertInAttachmentStage();
+ mListener = new FieldBinding(field);
+ }
+
+ /**
+ * Bind this input port to the field with the specified name.
+ *
+ * Use this when you wish to pull frames directly into a field of the filter. This requires
+ * that the input frames can be interpreted as object-based frames of the field's class.
+ * Overrides any previous bindings.
+ *
+ * This is typically called from inside
+ * {@link Filter#onInputPortAttached(InputPort) onInputPortAttached}, and used in
+ * conjunction with {@link #setAutoPullEnabled(boolean)}.
+ *
+ * @param fieldName the field to pull frame data into.
+ * @see #bindToField(Field)
+ * @see #setAutoPullEnabled(boolean)
+ */
+ public void bindToFieldNamed(String fieldName) {
+ Field field = findFieldNamed(fieldName, mFilter.getClass());
+ if (field == null) {
+ throw new IllegalArgumentException("Attempting to bind to unknown field '"
+ + fieldName + "'!");
+ }
+ bindToField(field);
+ }
+
+ /**
+ * Set whether the InputPort automatically pulls frames.
+ * This is typically only used when the port is bound to another target.
+ * @param enabled true, if frames should be automatically pulled on this port.
+ */
+ public void setAutoPullEnabled(boolean enabled) {
+ mAutoPullEnabled = enabled;
+ }
+
+ /**
+ * Returns whether the InputPort automatically pulls frames.
+ * @return true, if frames are automatically pulled on this port.
+ */
+ public boolean isAutoPullEnabled() {
+ return mAutoPullEnabled;
+ }
+
+ /**
+ * Pull a waiting a frame from the port.
+ *
+ * Call this to pull a frame from the input port for processing. If no frame is waiting on the
+ * input port, returns null. After this call the port will have no Frame waiting (empty port).
+ * Note, that this returns a frame owned by the input queue. You must detach the frame if you
+ * wish to hold on to it.
+ *
+ * @return Frame instance, or null if no frame is available for pulling.
+ */
+ public synchronized Frame pullFrame() {
+ if (mQueue == null) {
+ throw new IllegalStateException("Cannot pull frame from closed input port!");
+ }
+ Frame frame = mQueue.pullFrame();
+ if (frame != null) {
+ if (mListener != null) {
+ mListener.onFrameReceived(this, frame);
+ }
+ //Log.i("InputPort", "Adding frame " + frame + " to auto-release pool");
+ mFilter.addAutoReleaseFrame(frame);
+ long timestamp = frame.getTimestamp();
+ if (timestamp != Frame.TIMESTAMP_NOT_SET) {
+ mFilter.onPulledFrameWithTimestamp(frame.getTimestamp());
+ }
+ }
+ return frame;
+ }
+
+ public synchronized Frame peek() {
+ if (mQueue == null) {
+ throw new IllegalStateException("Cannot pull frame from closed input port!");
+ }
+ return mQueue.peek();
+ }
+
+ /**
+ * Returns true, if the port is connected.
+ * @return true, if there is an output port that connects to this port.
+ */
+ public boolean isConnected() {
+ return mQueue != null;
+ }
+
+ /**
+ * Returns true, if there is a frame waiting on this port.
+ * @return true, if there is a frame waiting on this port.
+ */
+ public synchronized boolean hasFrame() {
+ return mQueue != null && mQueue.canPull();
+ }
+
+ /**
+ * Sets whether to wait for a frame on this port before processing.
+ * When set to true, the Filter will not be scheduled for processing unless there is a Frame
+ * waiting on this port. The default value is true.
+ *
+ * @param wait true, if the Filter should wait for a Frame before processing.
+ * @see #waitsForFrame()
+ */
+ public void setWaitsForFrame(boolean wait) {
+ mWaitForFrame = wait;
+ }
+
+ /**
+ * Returns whether the filter waits for a frame on this port before processing.
+ * @return true, if the filter waits for a frame on this port before processing.
+ * @see #setWaitsForFrame(boolean)
+ */
+ public boolean waitsForFrame() {
+ return mWaitForFrame;
+ }
+
+ /**
+ * Returns the input port's name.
+ * This is the name that was specified when the input port was connected.
+ *
+ * @return the input port's name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the FrameType of this port.
+ * This is the type that was specified when the input port was declared.
+ *
+ * @return the input port's FrameType.
+ */
+ public FrameType getType() {
+ return getQueue().getType();
+ }
+
+ /**
+ * Return the filter object that this port belongs to.
+ *
+ * @return the input port's filter.
+ */
+ public Filter getFilter() {
+ return mFilter;
+ }
+
+ @Override
+ public String toString() {
+ return mFilter.getName() + ":" + mName;
+ }
+
+ // Internal only ///////////////////////////////////////////////////////////////////////////////
+ InputPort(Filter filter, String name, Signature.PortInfo info) {
+ mFilter = filter;
+ mName = name;
+ mInfo = info;
+ }
+
+ boolean conditionsMet() {
+ return !mWaitForFrame || hasFrame();
+ }
+
+ void onOpen(FrameQueue.Builder builder) {
+ mQueueBuilder = builder;
+ mQueueBuilder.setReadType(mInfo.type);
+ mFilter.onInputPortOpen(this);
+ }
+
+ void setQueue(FrameQueue queue) {
+ mQueue = queue;
+ mQueueBuilder = null;
+ }
+
+ FrameQueue getQueue() {
+ return mQueue;
+ }
+
+ void clear() {
+ if (mQueue != null) {
+ mQueue.clear();
+ }
+ }
+
+ private void assertInAttachmentStage() {
+ if (mQueueBuilder == null) {
+ throw new IllegalStateException("Attempting to attach port while not in attachment "
+ + "stage!");
+ }
+ }
+
+ private Field findFieldNamed(String fieldName, Class> clazz) {
+ Field field = null;
+ try {
+ field = clazz.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Class> superClass = clazz.getSuperclass();
+ if (superClass != null) {
+ field = findFieldNamed(fieldName, superClass);
+ }
+ }
+ return field;
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MffContext.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MffContext.java
new file mode 100644
index 0000000000000..b7212f982bce0
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MffContext.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.renderscript.RenderScript;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The MffContext holds the state and resources of a Mobile Filter Framework processing instance.
+ * Though it is possible to create multiple MffContext instances, typical applications will rely on
+ * a single MffContext to perform all processing within the Mobile Filter Framework.
+ *
+ * The MffContext class declares two methods {@link #onPause()} and {@link #onResume()}, that are
+ * typically called when the application activity is paused and resumed. This will take care of
+ * halting any processing in the context, and releasing resources while the activity is paused.
+ */
+public class MffContext {
+
+ /**
+ * Class to hold configuration information for MffContexts.
+ */
+ public static class Config {
+ /**
+ * Set to true, if this context will make use of the camera.
+ * If your application does not require the camera, the context does not guarantee that
+ * a camera is available for streaming. That is, you may only use a CameraStreamer if
+ * the context's {@link #isCameraStreamingSupported()} returns true.
+ */
+ public boolean requireCamera = true;
+
+ /**
+ * Set to true, if this context requires OpenGL.
+ * If your application does not require OpenGL, the context does not guarantee that OpenGL
+ * is available. That is, you may only use OpenGL (within filters running in this context)
+ * if the context's {@link #isOpenGLSupported()} method returns true.
+ */
+ public boolean requireOpenGL = true;
+
+ /**
+ * On older Android versions the Camera may need a SurfaceView to render into in order to
+ * function. You may specify a dummy SurfaceView here if you do not want the context to
+ * create its own view. Note, that your view may or may not be used. You cannot rely on
+ * your dummy view to be used by the Camera. If you pass null, no dummy view will be used.
+ * In this case your application may not run correctly on older devices if you use the
+ * camera. This flag has no effect if you do not require the camera.
+ */
+ public SurfaceView dummySurface = null;
+
+ /** Force MFF to not use OpenGL in its processing. */
+ public boolean forceNoGL = false;
+ }
+
+ static private class State {
+ public static final int STATE_RUNNING = 1;
+ public static final int STATE_PAUSED = 2;
+ public static final int STATE_DESTROYED = 3;
+
+ public int current = STATE_RUNNING;
+ }
+
+ /** The application context. */
+ private Context mApplicationContext = null;
+
+ /** The set of filter graphs within this context */
+ private Set mGraphs = new HashSet();
+
+ /** The set of graph runners within this context */
+ private Set mRunners = new HashSet();
+
+ /** True, if the context preserves frames when paused. */
+ private boolean mPreserveFramesOnPause = false;
+
+ /** The shared CameraStreamer that streams camera frames to CameraSource filters. */
+ private CameraStreamer mCameraStreamer = null;
+
+ /** The current context state. */
+ private State mState = new State();
+
+ /** A dummy SurfaceView that is required for Camera operation on older devices. */
+ private SurfaceView mDummySurfaceView = null;
+
+ /** Handler to execute code in the context's thread, such as issuing callbacks. */
+ private Handler mHandler = null;
+
+ /** Flag whether OpenGL ES 2 is supported in this context. */
+ private boolean mGLSupport;
+
+ /** Flag whether camera streaming is supported in this context. */
+ private boolean mCameraStreamingSupport;
+
+ /** RenderScript base master class. */
+ private RenderScript mRenderScript;
+
+ /**
+ * Creates a new MffContext with the default configuration.
+ *
+ * An MffContext must be attached to a Context object of an application. You may create
+ * multiple MffContexts, however data between them cannot be shared. The context must be
+ * created in a thread with a Looper (such as the main/UI thread).
+ *
+ * On older versions of Android, the MffContext may create a visible dummy view for the
+ * camera to render into. This is a 1x1 SurfaceView that is placed into the top-left corner.
+ *
+ * @param context The application context to attach the MffContext to.
+ */
+ public MffContext(Context context) {
+ init(context, new Config());
+ }
+
+ /**
+ * Creates a new MffContext with the specified configuration.
+ *
+ * An MffContext must be attached to a Context object of an application. You may create
+ * multiple MffContexts, however data between them cannot be shared. The context must be
+ * created in a thread with a Looper (such as the main/UI thread).
+ *
+ * On older versions of Android, the MffContext may create a visible dummy view for the
+ * camera to render into. This is a 1x1 SurfaceView that is placed into the top-left corner.
+ * You may alternatively specify your own SurfaceView in the configuration.
+ *
+ * @param context The application context to attach the MffContext to.
+ * @param config The configuration to use.
+ *
+ * @throws RuntimeException If no context for the requested configuration could be created.
+ */
+ public MffContext(Context context, Config config) {
+ init(context, config);
+ }
+
+ /**
+ * Put all processing in the context on hold.
+ * This is typically called from your application's onPause() method, and will
+ * stop all running graphs (closing their filters). If the context does not preserve frames on
+ * pause (see {@link #setPreserveFramesOnPause(boolean)}) all frames attached to this context
+ * are released.
+ */
+ public void onPause() {
+ synchronized (mState) {
+ if (mState.current == State.STATE_RUNNING) {
+ if (mCameraStreamer != null) {
+ mCameraStreamer.halt();
+ }
+ stopRunners(true);
+ mState.current = State.STATE_PAUSED;
+ }
+ }
+ }
+
+ /**
+ * Resumes the processing in this context.
+ * This is typically called from the application's onResume() method, and will
+ * resume processing any of the previously stopped filter graphs.
+ */
+ public void onResume() {
+ synchronized (mState) {
+ if (mState.current == State.STATE_PAUSED) {
+ resumeRunners();
+ resumeCamera();
+ mState.current = State.STATE_RUNNING;
+ }
+ }
+ }
+
+ /**
+ * Release all resources associated with this context.
+ * This will also stop any running graphs.
+ */
+ public void release() {
+ synchronized (mState) {
+ if (mState.current != State.STATE_DESTROYED) {
+ if (mCameraStreamer != null) {
+ mCameraStreamer.stop();
+ mCameraStreamer.tearDown();
+ }
+ if (Build.VERSION.SDK_INT >= 11) {
+ maybeDestroyRenderScript();
+ }
+ stopRunners(false);
+ waitUntilStopped();
+ tearDown();
+ mState.current = State.STATE_DESTROYED;
+ }
+ }
+ }
+
+ /**
+ * Set whether frames are preserved when the context is paused.
+ * When passing false, all Frames associated with this context are released. The default
+ * value is true.
+ *
+ * @param preserve true, to preserve frames when the context is paused.
+ *
+ * @see #getPreserveFramesOnPause()
+ */
+ public void setPreserveFramesOnPause(boolean preserve) {
+ mPreserveFramesOnPause = preserve;
+ }
+
+ /**
+ * Returns whether frames are preserved when the context is paused.
+ *
+ * @return true, if frames are preserved when the context is paused.
+ *
+ * @see #setPreserveFramesOnPause(boolean)
+ */
+ public boolean getPreserveFramesOnPause() {
+ return mPreserveFramesOnPause;
+ }
+
+ /**
+ * Returns the application context that the MffContext is attached to.
+ *
+ * @return The application context for this context.
+ */
+ public Context getApplicationContext() {
+ return mApplicationContext;
+ }
+
+ /**
+ * Returns the context's shared CameraStreamer.
+ * Use the CameraStreamer to control the Camera. Frames from the Camera are typically streamed
+ * to CameraSource filters.
+ *
+ * @return The context's CameraStreamer instance.
+ */
+ public CameraStreamer getCameraStreamer() {
+ if (mCameraStreamer == null) {
+ mCameraStreamer = new CameraStreamer(this);
+ }
+ return mCameraStreamer;
+ }
+
+ /**
+ * Set the default EGL config chooser.
+ *
+ * When an EGL context is required by the MFF, the channel sizes specified here are used. The
+ * default sizes are 8 bits per R,G,B,A channel and 0 bits for depth and stencil channels.
+ *
+ * @param redSize The size of the red channel in bits.
+ * @param greenSize The size of the green channel in bits.
+ * @param blueSize The size of the blue channel in bits.
+ * @param alphaSize The size of the alpha channel in bits.
+ * @param depthSize The size of the depth channel in bits.
+ * @param stencilSize The size of the stencil channel in bits.
+ */
+ public static void setEGLConfigChooser(int redSize,
+ int greenSize,
+ int blueSize,
+ int alphaSize,
+ int depthSize,
+ int stencilSize) {
+ RenderTarget.setEGLConfigChooser(redSize,
+ greenSize,
+ blueSize,
+ alphaSize,
+ depthSize,
+ stencilSize);
+ }
+
+ /**
+ * Returns true, if this context supports using OpenGL.
+ * @return true, if this context supports using OpenGL.
+ */
+ public final boolean isOpenGLSupported() {
+ return mGLSupport;
+ }
+
+ /**
+ * Returns true, if this context supports camera streaming.
+ * @return true, if this context supports camera streaming.
+ */
+ public final boolean isCameraStreamingSupported() {
+ return mCameraStreamingSupport;
+ }
+
+ @TargetApi(11)
+ public final RenderScript getRenderScript() {
+ if (mRenderScript == null) {
+ mRenderScript = RenderScript.create(mApplicationContext);
+ }
+ return mRenderScript;
+ }
+
+ final void assertOpenGLSupported() {
+ if (!isOpenGLSupported()) {
+ throw new RuntimeException("Attempting to use OpenGL ES 2 in a context that does not "
+ + "support it!");
+ }
+ }
+
+ void addGraph(FilterGraph graph) {
+ synchronized (mGraphs) {
+ mGraphs.add(graph);
+ }
+ }
+
+ void addRunner(GraphRunner runner) {
+ synchronized (mRunners) {
+ mRunners.add(runner);
+ }
+ }
+
+ SurfaceView getDummySurfaceView() {
+ return mDummySurfaceView;
+ }
+
+ void postRunnable(Runnable runnable) {
+ mHandler.post(runnable);
+ }
+
+ private void init(Context context, Config config) {
+ determineGLSupport(context, config);
+ determineCameraSupport(config);
+ createHandler();
+ mApplicationContext = context.getApplicationContext();
+ fetchDummySurfaceView(context, config);
+ }
+
+ private void fetchDummySurfaceView(Context context, Config config) {
+ if (config.requireCamera && CameraStreamer.requireDummySurfaceView()) {
+ mDummySurfaceView = config.dummySurface != null
+ ? config.dummySurface
+ : createDummySurfaceView(context);
+ }
+ }
+
+ private void determineGLSupport(Context context, Config config) {
+ if (config.forceNoGL) {
+ mGLSupport = false;
+ } else {
+ mGLSupport = getPlatformSupportsGLES2(context);
+ if (config.requireOpenGL && !mGLSupport) {
+ throw new RuntimeException("Cannot create context that requires GL support on "
+ + "this platform!");
+ }
+ }
+ }
+
+ private void determineCameraSupport(Config config) {
+ mCameraStreamingSupport = (CameraStreamer.getNumberOfCameras() > 0);
+ if (config.requireCamera && !mCameraStreamingSupport) {
+ throw new RuntimeException("Cannot create context that requires a camera on "
+ + "this platform!");
+ }
+ }
+
+ private static boolean getPlatformSupportsGLES2(Context context) {
+ ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ ConfigurationInfo configurationInfo = am.getDeviceConfigurationInfo();
+ return configurationInfo.reqGlEsVersion >= 0x20000;
+ }
+
+ private void createHandler() {
+ if (Looper.myLooper() == null) {
+ throw new RuntimeException("MffContext must be created in a thread with a Looper!");
+ }
+ mHandler = new Handler();
+ }
+
+ private void stopRunners(boolean haltOnly) {
+ synchronized (mRunners) {
+ // Halt all runners (does nothing if not running)
+ for (GraphRunner runner : mRunners) {
+ if (haltOnly) {
+ runner.halt();
+ } else {
+ runner.stop();
+ }
+ }
+ // Flush all graphs if requested (this is queued up after the call to halt)
+ if (!mPreserveFramesOnPause) {
+ for (GraphRunner runner : mRunners) {
+ runner.flushFrames();
+ }
+ }
+ }
+ }
+
+ private void resumeRunners() {
+ synchronized (mRunners) {
+ for (GraphRunner runner : mRunners) {
+ runner.restart();
+ }
+ }
+ }
+
+ private void resumeCamera() {
+ // Restart only affects previously halted cameras that were running.
+ if (mCameraStreamer != null) {
+ mCameraStreamer.restart();
+ }
+ }
+
+ private void waitUntilStopped() {
+ for (GraphRunner runner : mRunners) {
+ runner.waitUntilStop();
+ }
+ }
+
+ private void tearDown() {
+ // Tear down graphs
+ for (FilterGraph graph : mGraphs) {
+ graph.tearDown();
+ }
+
+ // Tear down runners
+ for (GraphRunner runner : mRunners) {
+ runner.tearDown();
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private SurfaceView createDummySurfaceView(Context context) {
+ // This is only called on Gingerbread devices, so deprecation warning is unnecessary.
+ SurfaceView dummySurfaceView = new SurfaceView(context);
+ dummySurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+ // If we have an activity for this context we'll add the SurfaceView to it (as a 1x1 view
+ // in the top-left corner). If not, we warn the user that they may need to add one manually.
+ Activity activity = findActivityForContext(context);
+ if (activity != null) {
+ ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(1, 1);
+ activity.addContentView(dummySurfaceView, params);
+ } else {
+ Log.w("MffContext", "Could not find activity for dummy surface! Consider specifying "
+ + "your own SurfaceView!");
+ }
+ return dummySurfaceView;
+ }
+
+ private Activity findActivityForContext(Context context) {
+ return (context instanceof Activity) ? (Activity) context : null;
+ }
+
+ @TargetApi(11)
+ private void maybeDestroyRenderScript() {
+ if (mRenderScript != null) {
+ mRenderScript.destroy();
+ mRenderScript = null;
+ }
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MotionSensor.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MotionSensor.java
new file mode 100644
index 0000000000000..95558f27e4249
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MotionSensor.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Make values from a motion sensor (e.g., accelerometer) available as filter outputs.
+
+package androidx.media.filterpacks.sensors;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.FrameValues;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.OutputPort;
+import androidx.media.filterfw.Signature;
+
+public final class MotionSensor extends Filter implements SensorEventListener {
+
+ private SensorManager mSensorManager = null;
+ private Sensor mSensor = null;
+
+ private float[] mValues = new float[3];
+
+ public MotionSensor(MffContext context, String name) {
+ super(context, name);
+ }
+
+ @Override
+ public Signature getSignature() {
+ return new Signature()
+ .addOutputPort("values", Signature.PORT_REQUIRED, FrameType.array(float.class))
+ .disallowOtherPorts();
+ }
+
+ @Override
+ protected void onPrepare() {
+ mSensorManager = (SensorManager)getContext().getApplicationContext()
+ .getSystemService(Context.SENSOR_SERVICE);
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
+ // TODO: currently, the type of sensor is hardcoded. Should be able to set the sensor
+ // type as filter input!
+ mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_UI);
+ }
+
+ @Override
+ protected void onTearDown() {
+ mSensorManager.unregisterListener(this);
+ }
+
+ @Override
+ public final void onAccuracyChanged(Sensor sensor, int accuracy) {
+ // (Do we need to do something when sensor accuracy changes?)
+ }
+
+ @Override
+ public final void onSensorChanged(SensorEvent event) {
+ synchronized(mValues) {
+ mValues[0] = event.values[0];
+ mValues[1] = event.values[1];
+ mValues[2] = event.values[2];
+ }
+ }
+
+ @Override
+ protected void onProcess() {
+ OutputPort outPort = getConnectedOutputPort("values");
+ FrameValues outFrame = outPort.fetchAvailableFrame(null).asFrameValues();
+ synchronized(mValues) {
+ outFrame.setValues(mValues);
+ }
+ outFrame.setTimestamp(System.currentTimeMillis() * 1000000L);
+ outPort.pushFrame(outFrame);
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NewChromaHistogramFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NewChromaHistogramFilter.java
new file mode 100644
index 0000000000000..f524be371a3b7
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NewChromaHistogramFilter.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Extract histogram from image.
+
+package androidx.media.filterpacks.histogram;
+
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.Frame;
+import androidx.media.filterfw.FrameBuffer2D;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.InputPort;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.OutputPort;
+import androidx.media.filterfw.Signature;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+/**
+ * ChromaHistogramFilter takes in an image in HSVA format and computes a 2-D histogram with a
+ * 2 dimensional chroma histogram based on hue (column) and saturation (row) at the top and
+ * a 1-D value histogram in the last row. The number of bin in the value histogram equals to
+ * the number of bins in hue.
+ */
+public final class NewChromaHistogramFilter extends Filter {
+
+ private int mHueBins = 6;
+ private int mSaturationBins = 3;
+ private int mValueBins;
+
+ private int mSaturationThreshold = 26; // 255 * 0.1
+ private int mValueThreshold = 51; // 255 * 0.2
+
+ public NewChromaHistogramFilter(MffContext context, String name) {
+ super(context, name);
+ }
+
+ @Override
+ public Signature getSignature() {
+ FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU);
+ FrameType dataOut = FrameType.buffer2D(FrameType.ELEMENT_FLOAT32);
+
+ return new Signature()
+ .addInputPort("image", Signature.PORT_REQUIRED, imageIn)
+ .addInputPort("huebins", Signature.PORT_OPTIONAL, FrameType.single(int.class))
+ .addInputPort("saturationbins", Signature.PORT_OPTIONAL, FrameType.single(int.class))
+ .addInputPort("saturationthreshold", Signature.PORT_OPTIONAL,
+ FrameType.single(int.class))
+ .addInputPort("valuethreshold", Signature.PORT_OPTIONAL, FrameType.single(int.class))
+ .addOutputPort("histogram", Signature.PORT_REQUIRED, dataOut)
+ .disallowOtherPorts();
+ }
+
+ @Override
+ public void onInputPortOpen(InputPort port) {
+ if (port.getName().equals("huebins")) {
+ port.bindToFieldNamed("mHueBins");
+ port.setAutoPullEnabled(true);
+ } else if (port.getName().equals("saturationbins")) {
+ port.bindToFieldNamed("mSaturationBins");
+ port.setAutoPullEnabled(true);
+ } else if (port.getName().equals("saturationthreshold")) {
+ port.bindToFieldNamed("mSaturationThreshold");
+ port.setAutoPullEnabled(true);
+ } else if (port.getName().equals("valuethreshold")) {
+ port.bindToFieldNamed("mValueThreshold");
+ port.setAutoPullEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onProcess() {
+ FrameBuffer2D imageFrame = getConnectedInputPort("image").pullFrame().asFrameImage2D();
+ OutputPort outPort = getConnectedOutputPort("histogram");
+
+ mValueBins = mHueBins;
+ int[] outDims = new int[] {mHueBins, mSaturationBins + 1};
+ FrameBuffer2D histogramFrame = outPort.fetchAvailableFrame(outDims).asFrameBuffer2D();
+
+ ByteBuffer imageBuffer = imageFrame.lockBytes(Frame.MODE_READ);
+ ByteBuffer histogramBuffer = histogramFrame.lockBytes(Frame.MODE_READ);
+ histogramBuffer.order(ByteOrder.nativeOrder());
+ FloatBuffer floatHistogram = histogramBuffer.asFloatBuffer();
+
+ // Run native method
+ extractChromaHistogram(imageBuffer, floatHistogram, mHueBins, mSaturationBins, mValueBins,
+ mSaturationThreshold, mValueThreshold);
+
+ imageFrame.unlock();
+ histogramFrame.unlock();
+
+ outPort.pushFrame(histogramFrame);
+ }
+
+ private static native void extractChromaHistogram(ByteBuffer imageBuffer,
+ FloatBuffer histogramBuffer, int hueBins, int saturationBins, int valueBins,
+ int saturationThreshold, int valueThreshold);
+
+ static {
+ System.loadLibrary("smartcamera_jni");
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NormFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NormFilter.java
new file mode 100644
index 0000000000000..e81611076e4b8
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NormFilter.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterpacks.numeric;
+
+import android.util.Log;
+import androidx.media.filterfw.Filter;
+import androidx.media.filterfw.FrameType;
+import androidx.media.filterfw.FrameValue;
+import androidx.media.filterfw.MffContext;
+import androidx.media.filterfw.OutputPort;
+import androidx.media.filterfw.Signature;
+
+/**
+ * Filter to calculate the 2-norm of the inputs. i.e. sqrt(x^2 + y^2)
+ * TODO: Add support for more norms in the future.
+ */
+public final class NormFilter extends Filter {
+ private static final String TAG = "NormFilter";
+ private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
+
+ public NormFilter(MffContext context, String name) {
+ super(context, name);
+ }
+
+ @Override
+ public Signature getSignature() {
+ FrameType floatT = FrameType.single(float.class);
+ return new Signature()
+ .addInputPort("x", Signature.PORT_REQUIRED, floatT)
+ .addInputPort("y", Signature.PORT_REQUIRED, floatT)
+ .addOutputPort("norm", Signature.PORT_REQUIRED, floatT)
+ .disallowOtherPorts();
+ }
+
+ @Override
+ protected void onProcess() {
+ FrameValue xFrameValue = getConnectedInputPort("x").pullFrame().asFrameValue();
+ float xValue = ((Float)xFrameValue.getValue()).floatValue();
+ FrameValue yFrameValue = getConnectedInputPort("y").pullFrame().asFrameValue();
+ float yValue = ((Float)yFrameValue.getValue()).floatValue();
+
+ float norm = (float) Math.hypot(xValue, yValue);
+ if (mLogVerbose) Log.v(TAG, "Norm = " + norm);
+ OutputPort outPort = getConnectedOutputPort("norm");
+ FrameValue outFrame = outPort.fetchAvailableFrame(null).asFrameValue();
+ outFrame.setValue(norm);
+ outPort.pushFrame(outFrame);
+ }
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/OutputPort.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/OutputPort.java
new file mode 100644
index 0000000000000..06117c3df5ccf
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/OutputPort.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+/**
+ * Output ports are the data emitting ports of filters.
+ *
+ * Filters push data frames onto output-ports, which in turn push them onto their connected input
+ * ports. Output ports must be connected to an input port before data can be pushed onto them.
+ * Input and output ports share their Frame slot, meaning that when a frame is waiting on an output
+ * port, it is also waiting on the connected input port.
+ *
+ * Only one frame can be pushed onto an output port at a time. In other words, a Frame must first
+ * be consumed by the target filter before a new frame can be pushed on the output port. If the
+ * output port is set to wait until it becomes free (see {@link #setWaitsUntilAvailable(boolean)}),
+ * it is guaranteed to be available when {@code onProcess()} is called. This is the default setting.
+ *
+ */
+public final class OutputPort {
+
+ private Filter mFilter;
+ private String mName;
+ private Signature.PortInfo mInfo;
+ private FrameQueue.Builder mQueueBuilder = null;
+ private FrameQueue mQueue = null;
+ private boolean mWaitsUntilAvailable = true;
+ private InputPort mTarget = null;
+
+ /**
+ * Returns true, if this port is connected to a target port.
+ * @return true, if this port is connected to a target port.
+ */
+ public boolean isConnected() {
+ return mTarget != null;
+ }
+
+ /**
+ * Returns true, if there is no frame waiting on this port.
+ * @return true, if no Frame instance is waiting on this port.
+ */
+ public boolean isAvailable() {
+ return mQueue == null || mQueue.canPush();
+ }
+
+ /**
+ * Returns a frame for writing.
+ *
+ * Call this method to fetch a new frame to write into. When you have finished writing the
+ * frame data, you can push it into the output queue using {@link #pushFrame(Frame)}. Note,
+ * that the Frame returned is owned by the queue. If you wish to hold on to the frame, you
+ * must detach it.
+ *
+ * @param dimensions the size of the Frame you wish to obtain.
+ * @return a writable Frame instance.
+ */
+ public Frame fetchAvailableFrame(int[] dimensions) {
+ Frame frame = getQueue().fetchAvailableFrame(dimensions);
+ if (frame != null) {
+ //Log.i("OutputPort", "Adding frame " + frame + " to auto-release pool");
+ mFilter.addAutoReleaseFrame(frame);
+ }
+ return frame;
+ }
+
+ /**
+ * Pushes a frame onto this output port.
+ *
+ * This is typically a Frame instance you obtained by previously calling
+ * {@link #fetchAvailableFrame(int[])}, but may come from other sources such as an input port
+ * that is attached to this output port.
+ *
+ * Once you have pushed a frame to an output, you may no longer modify it as it may be shared
+ * among other filters.
+ *
+ * @param frame the frame to push to the output queue.
+ */
+ public void pushFrame(Frame frame) {
+ // Some queues allow pushing without fetching, so we need to make sure queue is open
+ // before pushing!
+ long timestamp = frame.getTimestamp();
+ if (timestamp == Frame.TIMESTAMP_NOT_SET)
+ frame.setTimestamp(mFilter.getCurrentTimestamp());
+ getQueue().pushFrame(frame);
+ }
+
+ /**
+ * Sets whether to wait until this port becomes available before processing.
+ * When set to true, the Filter will not be scheduled for processing unless there is no Frame
+ * waiting on this port. The default value is true.
+ *
+ * @param wait true, if filter should wait for the port to become available before processing.
+ * @see #waitsUntilAvailable()
+ */
+ public void setWaitsUntilAvailable(boolean wait) {
+ mWaitsUntilAvailable = wait;
+ }
+
+ /**
+ * Returns whether the filter waits until this port is available before processing.
+ * @return true, if the filter waits until this port is available before processing.
+ * @see #setWaitsUntilAvailable(boolean)
+ */
+ public boolean waitsUntilAvailable() {
+ return mWaitsUntilAvailable;
+ }
+
+ /**
+ * Returns the output port's name.
+ * This is the name that was specified when the output port was connected.
+ *
+ * @return the output port's name.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Return the filter object that this port belongs to.
+ *
+ * @return the output port's filter.
+ */
+ public Filter getFilter() {
+ return mFilter;
+ }
+
+ @Override
+ public String toString() {
+ return mFilter.getName() + ":" + mName;
+ }
+
+ OutputPort(Filter filter, String name, Signature.PortInfo info) {
+ mFilter = filter;
+ mName = name;
+ mInfo = info;
+ }
+
+ void setTarget(InputPort target) {
+ mTarget = target;
+ }
+
+ /**
+ * Return the (input) port that this output port is connected to.
+ *
+ * @return the connected port, null if not connected.
+ */
+ public InputPort getTarget() {
+ return mTarget;
+ }
+
+ FrameQueue getQueue() {
+ return mQueue;
+ }
+
+ void setQueue(FrameQueue queue) {
+ mQueue = queue;
+ mQueueBuilder = null;
+ }
+
+ void onOpen(FrameQueue.Builder builder) {
+ mQueueBuilder = builder;
+ mQueueBuilder.setWriteType(mInfo.type);
+ mFilter.onOutputPortOpen(this);
+ }
+
+ boolean isOpen() {
+ return mQueue != null;
+ }
+
+ final boolean conditionsMet() {
+ return !mWaitsUntilAvailable || isAvailable();
+ }
+
+ void clear() {
+ if (mQueue != null) {
+ mQueue.clear();
+ }
+ }
+}
+
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/PixelUtils.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/PixelUtils.java
new file mode 100644
index 0000000000000..88538d42b00ff
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/PixelUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media.filterfw;
+
+import java.nio.ByteBuffer;
+
+/**
+ * A collection of utilities to deal with pixel operations on ByteBuffers.
+ */
+public class PixelUtils {
+
+ /**
+ * Copy pixels from one buffer to another, applying a transformation.
+ *
+ *
The transformation is specified by specifying the initial offset in the output buffer, the
+ * stride (in pixels) between each pixel, and the stride (in pixels) between each row. The row
+ * stride is measured as the number of pixels between the start of each row.
+ *
+ *
Note that this method is native for efficiency reasons. It does NOT do any bounds checking
+ * other than making sure the buffers are of sufficient size. This means that you can corrupt
+ * memory if specifying incorrect stride values!
+ *
+ * @param input The input buffer containing pixel data.
+ * @param output The output buffer to hold the transformed pixel data.
+ * @param width The width of the input image.
+ * @param height The height of the input image.
+ * @param offset The start offset in the output (in pixels)
+ * @param pixStride The stride between each pixel (in pixels)
+ * @param rowStride The stride between the start of each row (in pixels)
+ */
+ public static void copyPixels(ByteBuffer input,
+ ByteBuffer output,
+ int width,
+ int height,
+ int offset,
+ int pixStride,
+ int rowStride) {
+ if (input.remaining() != output.remaining()) {
+ throw new IllegalArgumentException("Input and output buffers must have the same size!");
+ } else if (input.remaining() % 4 != 0) {
+ throw new IllegalArgumentException("Input buffer size must be a multiple of 4!");
+ } else if (output.remaining() % 4 != 0) {
+ throw new IllegalArgumentException("Output buffer size must be a multiple of 4!");
+ } else if ((width * height * 4) != input.remaining()) {
+ throw new IllegalArgumentException(
+ "Input buffer size does not match given dimensions!");
+ } else if ((width * height * 4) != output.remaining()) {
+ throw new IllegalArgumentException(
+ "Output buffer size does not match given dimensions!");
+ }
+ nativeCopyPixels(input, output, width, height, offset, pixStride, rowStride);
+ }
+
+ private static native void nativeCopyPixels(ByteBuffer input,
+ ByteBuffer output,
+ int width,
+ int height,
+ int offset,
+ int pixStride,
+ int rowStride);
+
+ static {
+ System.loadLibrary("smartcamera_jni");
+ }
+
+}
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RenderTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RenderTarget.java
new file mode 100644
index 0000000000000..ab0546dbbf089
--- /dev/null
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RenderTarget.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.media.filterfw;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.media.MediaRecorder;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.os.Build.VERSION;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+
+public final class RenderTarget {
+
+ private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+ private static final int EGL_OPENGL_ES2_BIT = 4;
+
+ // Pre-HC devices do not necessarily support multiple display surfaces.
+ private static boolean mSupportsMultipleDisplaySurfaces = (VERSION.SDK_INT >= 11);
+
+ /** A Map that tracks which objects are wrapped by EGLSurfaces */
+ private static HashMap