├── .gitignore ├── README.md ├── app ├── .gitignore ├── CMakeLists.txt ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tomoima │ │ └── multicamerasample │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ └── native-lib.cpp │ ├── java │ │ └── com │ │ │ └── tomoima │ │ │ └── multicamerasample │ │ │ ├── CameraActivity.kt │ │ │ ├── CameraFragment.kt │ │ │ ├── extensions │ │ │ └── CameraCharacteristicUtil.kt │ │ │ ├── listeners │ │ │ └── SurfaceTextureWaiter.kt │ │ │ ├── models │ │ │ ├── CameraIdInfo.kt │ │ │ └── SurfaceTextureInfo.kt │ │ │ ├── services │ │ │ ├── Camera.kt │ │ │ └── CompareSizesByArea.kt │ │ │ └── ui │ │ │ ├── AutoFitTextureView.kt │ │ │ ├── ConfirmationDialog.kt │ │ │ └── ErrorDialog.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_camera.xml │ │ ├── fragment_camera.xml │ │ └── fragment_camera_2.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── tomoima │ └── multicamerasample │ └── ExampleUnitTest.kt ├── art ├── demo1.png ├── demo2.png └── demo3.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ related 2 | /captures 3 | .externalNativeBuild 4 | 5 | # Mac OS 6 | .DS_store 7 | 8 | # Built application files 9 | *.apk 10 | *.ap_ 11 | 12 | # Files for the ART/Dalvik VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | out/ 22 | 23 | # Gradle files 24 | .gradle/ 25 | build/ 26 | 27 | # Local configuration file (sdk path, etc) 28 | local.properties 29 | 30 | # Log Files 31 | *.log 32 | 33 | # IntelliJ 34 | *.iml 35 | .idea/ 36 | projectFilesBackup/ 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Sample App to demonstrate Camera2 multi-camera API 3 | 4 | ** Caution!!: This sample only runs upon Pixel3 ** 5 | 6 | ## Requirements 7 | - Android NDK (download from `Preferences` -> `Appearance & Behavior` -> `System Settings` -> `Android SDK` ) This is actually not required for this project but I have a plan to add OpenCV so cpp setting is currently in the project. 8 | 9 | There are 2 modes available: 10 | 11 | if you want to test, switch the value below 12 | ``` 13 | private val zoomTransition = true 14 | ``` 15 | 16 | 1) 2 simultaneous camera steam view 17 | ![demo1](./art/demo1.png) 18 | 19 | 2) Zoom with 2 cameras 20 | ![demo2](./art/demo2.png) 21 | ![demo2](./art/demo3.png) 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS 2 | .DS_store 3 | 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the ART/Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | out/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Log Files 27 | *.log 28 | 29 | # IntelliJ 30 | *.iml 31 | .idea/ 32 | projectFilesBackup/ 33 | -------------------------------------------------------------------------------- /app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.4.1) 7 | 8 | # Creates and names a library, sets it as either STATIC 9 | # or SHARED, and provides the relative paths to its source code. 10 | # You can define multiple libraries, and CMake builds them for you. 11 | # Gradle automatically packages shared libraries with your APK. 12 | 13 | add_library( # Sets the name of the library. 14 | native-lib 15 | 16 | # Sets the library as a shared library. 17 | SHARED 18 | 19 | # Provides a relative path to your source file(s). 20 | src/main/cpp/native-lib.cpp) 21 | 22 | # Searches for a specified prebuilt library and stores the path as a 23 | # variable. Because CMake includes system libraries in the search path by 24 | # default, you only need to specify the name of the public NDK library 25 | # you want to add. CMake verifies that the library exists before 26 | # completing its build. 27 | 28 | find_library( # Sets the name of the path variable. 29 | log-lib 30 | 31 | # Specifies the name of the NDK library that 32 | # you want CMake to locate. 33 | log) 34 | 35 | # Specifies libraries CMake should link to your target library. You 36 | # can link multiple libraries, such as libraries you define in this 37 | # build script, prebuilt third-party libraries, or system libraries. 38 | 39 | target_link_libraries( # Specifies the target library. 40 | native-lib 41 | 42 | # Links the target library to the log library 43 | # included in the NDK. 44 | ${log-lib}) -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | applicationId "com.tomoima.multicamerasample" 11 | minSdkVersion 24 12 | targetSdkVersion 28 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | externalNativeBuild { 17 | cmake { 18 | cppFlags "" 19 | } 20 | } 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | externalNativeBuild { 29 | cmake { 30 | path "CMakeLists.txt" 31 | } 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation fileTree(dir: 'libs', include: ['*.jar']) 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 38 | implementation 'com.android.support:appcompat-v7:28.0.0' 39 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 40 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1' 41 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1' 42 | testImplementation 'junit:junit:4.12' 43 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 44 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 45 | } 46 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tomoima/multicamerasample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.tomoima.multicamerasample", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/cpp/native-lib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | extern "C" JNIEXPORT jstring JNICALL 5 | Java_com_tomoima_multicamerasample_CameraActivity_stringFromJNI( 6 | JNIEnv *env, 7 | jobject /* this */) { 8 | std::string hello = "Hello from C++"; 9 | return env->NewStringUTF(hello.c_str()); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/CameraActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample 2 | 3 | import android.support.v7.app.AppCompatActivity 4 | import android.os.Bundle 5 | import kotlinx.android.synthetic.main.activity_camera.* 6 | 7 | class CameraActivity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_camera) 12 | 13 | savedInstanceState ?: supportFragmentManager.beginTransaction() 14 | .replace(R.id.container, CameraFragment.newInstance()) 15 | .commit() 16 | } 17 | 18 | 19 | // I might use it for OpenCV 20 | /** 21 | * A native method that is implemented by the 'native-lib' native library, 22 | * which is packaged with this application. 23 | */ 24 | external fun stringFromJNI(): String 25 | 26 | companion object { 27 | 28 | // Used to load the 'native-lib' library on application startup. 29 | init { 30 | System.loadLibrary("native-lib") 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/CameraFragment.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.graphics.Matrix 7 | import android.graphics.RectF 8 | import android.graphics.SurfaceTexture 9 | import android.hardware.camera2.CameraAccessException 10 | import android.hardware.camera2.CameraManager 11 | import android.os.Bundle 12 | import android.os.Handler 13 | import android.support.v4.app.Fragment 14 | import android.support.v4.content.ContextCompat 15 | import android.util.Log 16 | import android.util.Size 17 | import android.view.* 18 | import android.widget.SeekBar 19 | import com.tomoima.multicamerasample.listeners.SurfaceTextureWaiter 20 | import kotlinx.android.synthetic.main.fragment_camera.* 21 | import com.tomoima.multicamerasample.models.CameraIdInfo 22 | import com.tomoima.multicamerasample.models.State 23 | import com.tomoima.multicamerasample.services.Camera 24 | import com.tomoima.multicamerasample.ui.ConfirmationDialog 25 | import com.tomoima.multicamerasample.ui.ErrorDialog 26 | import kotlinx.coroutines.Dispatchers 27 | import kotlinx.coroutines.GlobalScope 28 | import kotlinx.coroutines.launch 29 | import kotlinx.coroutines.withContext 30 | import kotlin.math.roundToInt 31 | 32 | class CameraFragment : Fragment() { 33 | 34 | companion object { 35 | private const val FRAGMENT_DIALOG = "dialog" 36 | private const val REQUEST_CAMERA_PERMISSION = 100 37 | private val TAG = CameraFragment.javaClass::getSimpleName.toString() 38 | fun newInstance() = CameraFragment() 39 | } 40 | 41 | private var camera: Camera? = null 42 | 43 | private lateinit var previewSize: Size 44 | 45 | private val zoomTransition = true 46 | 47 | override fun onCreateView( 48 | inflater: LayoutInflater, 49 | container: ViewGroup?, 50 | savedInstanceState: Bundle? 51 | ): View? = if(zoomTransition) { 52 | inflater.inflate(R.layout.fragment_camera_2, container, false) 53 | } else { 54 | inflater.inflate(R.layout.fragment_camera, container, false) 55 | } 56 | 57 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 58 | super.onViewCreated(view, savedInstanceState) 59 | 60 | zoomBar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener { 61 | var progressValue = 0 62 | override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 63 | this.progressValue = progress 64 | camera?.maxZoom?.let { 65 | if(!camera1View.isAvailable || !camera2View.isAvailable ) return@let 66 | val zoomValue = progressValue.toDouble()/seekBar.max * it 67 | if(zoomTransition) { 68 | // ADHOC 69 | // Because max zoom is 7 and zoom level difference of two cameras are 2, 70 | // Switch Camera 1 and 2 when zoom value is 100 / 7 * 2 = 2.857 71 | if (zoomValue < 2.857) { 72 | camera?.setZoom(zoomValue + 1) 73 | // Delay view switch to ease the transition 74 | Handler().postDelayed( 75 | { 76 | camera1ViewLayout.visibility = View.INVISIBLE 77 | camera2ViewLayout.visibility = View.VISIBLE 78 | } 79 | , 200) 80 | 81 | } else { 82 | camera?.setZoom(zoomValue) 83 | Handler().postDelayed( 84 | { 85 | camera1ViewLayout.visibility = View.VISIBLE 86 | camera2ViewLayout.visibility = View.INVISIBLE 87 | } 88 | , 200) 89 | } 90 | } else { 91 | camera?.setZoom(zoomValue) 92 | } 93 | } 94 | } 95 | 96 | override fun onStartTrackingTouch(seekBar: SeekBar?) { 97 | } 98 | 99 | override fun onStopTrackingTouch(seekBar: SeekBar) { 100 | } 101 | 102 | }) 103 | } 104 | 105 | override fun onActivityCreated(savedInstanceState: Bundle?) { 106 | super.onActivityCreated(savedInstanceState) 107 | val manager = activity!!.getSystemService(Context.CAMERA_SERVICE) as CameraManager 108 | camera = Camera.initInstance(manager) 109 | // set Seek bar zoom 110 | camera?.maxZoom?.let { 111 | val actualProgress = (100 / it).roundToInt() 112 | Log.d(TAG, "===== actual $actualProgress") 113 | zoomBar.progress = actualProgress 114 | } 115 | } 116 | 117 | override fun onResume() { 118 | super.onResume() 119 | if (camera1View.isAvailable && camera2View.isAvailable) { 120 | openCamera(camera1View.width, camera1View.height) 121 | } else { 122 | val waiter1 = SurfaceTextureWaiter(camera1View) 123 | val waiter2 = SurfaceTextureWaiter(camera2View) 124 | 125 | GlobalScope.launch { 126 | val result1 = waiter1.textureIsReady() 127 | val result2 = waiter2.textureIsReady() 128 | Log.d(TAG," ======== ready $result1 $result2" ) 129 | // Assuming both textures are ready and they have the same width and height, 130 | // Just check the state of 1 131 | when(result1.state) { 132 | State.ON_TEXTURE_AVAILABLE -> { 133 | withContext(Dispatchers.Main) { 134 | openDualCamera(width = result1.width, height = result1.height) 135 | } 136 | } 137 | State.ON_TEXTURE_SIZE_CHANGED -> { 138 | withContext(Dispatchers.Main) { 139 | configureTransform(viewWidth = result1.width, viewHeight = result2.height) 140 | } 141 | } 142 | else -> { } 143 | } 144 | } 145 | //camera1View.surfaceTextureListener = surfaceTextureListener 146 | //camera2View.surfaceTextureListener = surfaceTextureListener 147 | } 148 | } 149 | 150 | override fun onPause() { 151 | super.onPause() 152 | camera?.close() 153 | } 154 | 155 | // Permissions 156 | private fun requestCameraPermission() { 157 | if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { 158 | ConfirmationDialog().show(childFragmentManager, FRAGMENT_DIALOG) 159 | } else { 160 | requestPermissions( 161 | arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), 162 | REQUEST_CAMERA_PERMISSION 163 | ) 164 | } 165 | } 166 | 167 | override fun onRequestPermissionsResult( 168 | requestCode: Int, 169 | permissions: Array, 170 | grantResults: IntArray 171 | ) { 172 | if (requestCode == REQUEST_CAMERA_PERMISSION) { 173 | if (grantResults.size != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { 174 | ErrorDialog.newInstance(getString(R.string.request_permission)) 175 | .show(childFragmentManager, FRAGMENT_DIALOG) 176 | } 177 | } else { 178 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 179 | } 180 | } 181 | 182 | private fun openCamera(width: Int, height: Int) { 183 | if (activity == null) { 184 | Log.e(TAG, "activity is not ready!") 185 | return 186 | } 187 | val permission = ContextCompat.checkSelfPermission(activity!!, Manifest.permission.CAMERA) 188 | if (permission != PackageManager.PERMISSION_GRANTED) { 189 | requestCameraPermission() 190 | return 191 | } 192 | 193 | try { 194 | camera?.let { 195 | // Usually preview size has to be calculated based on the sensor rotation using getImageOrientation() 196 | // so that the sensor rotation and image rotation aspect matches correctly. 197 | // In this sample app, we know that Pixel series has the 90 degrees of sensor rotation, 198 | // so we just consider that width/ height < 1, which means portrait. 199 | val aspectRatio: Float = width / height.toFloat() 200 | previewSize = it.getPreviewSize(aspectRatio) 201 | camera1View.setAspectRatio(previewSize.height, previewSize.width) 202 | configureTransform(width, height) 203 | it.open() 204 | val texture1 = camera1View.surfaceTexture 205 | texture1.setDefaultBufferSize(previewSize.width, previewSize.height) 206 | it.start(listOf(Surface(texture1))) 207 | updateCameraStatus(it.getCameraIds()) 208 | } 209 | } catch (e: CameraAccessException) { 210 | Log.e(TAG, e.toString()) 211 | } catch (e: InterruptedException) { 212 | throw RuntimeException("Interrupted while trying to lock camera opening.", e) 213 | } 214 | } 215 | 216 | private fun openDualCamera(width: Int, height: Int) { 217 | if (activity == null) { 218 | Log.e(TAG, "activity is not ready!") 219 | return 220 | } 221 | val permission = ContextCompat.checkSelfPermission(activity!!, Manifest.permission.CAMERA) 222 | if (permission != PackageManager.PERMISSION_GRANTED) { 223 | requestCameraPermission() 224 | return 225 | } 226 | 227 | try { 228 | camera?.let { 229 | // Usually preview size has to be calculated based on the sensor rotation using getImageOrientation() 230 | // so that the sensor rotation and image rotation aspect matches correctly. 231 | // In this sample app, we know that Pixel series has the 90 degrees of sensor rotation, 232 | // so we just consider that width/ height < 1, which means portrait. 233 | val aspectRatio: Float = width / height.toFloat() 234 | previewSize = it.getPreviewSize(aspectRatio) 235 | // FIXME should write this better 236 | camera1View.setAspectRatio(previewSize.height, previewSize.width) 237 | camera2View.setAspectRatio(previewSize.height, previewSize.width) 238 | val matrix = calculateTransform(width, height) 239 | camera1View.setTransform(matrix) 240 | camera2View.setTransform(matrix) 241 | it.open() 242 | val texture1 = camera1View.surfaceTexture 243 | val texture2 = camera2View.surfaceTexture 244 | texture1.setDefaultBufferSize(previewSize.width, previewSize.height) 245 | texture2.setDefaultBufferSize(previewSize.width, previewSize.height) 246 | it.start(listOf(Surface(texture1), Surface(texture2))) 247 | updateCameraStatus(it.getCameraIds()) 248 | } 249 | } catch (e: CameraAccessException) { 250 | Log.e(TAG, e.toString()) 251 | } catch (e: InterruptedException) { 252 | throw RuntimeException("Interrupted while trying to lock camera opening.", e) 253 | } 254 | } 255 | 256 | private fun updateCameraStatus(cameraIdInfo: CameraIdInfo) { 257 | val (logicalCameraId, physicalCameraIds) = cameraIdInfo 258 | if(logicalCameraId.isNotEmpty()) { 259 | multiCameraSupportTv.text = "YES" 260 | logicalCameraTv.text = "[$logicalCameraId]" 261 | } 262 | if(physicalCameraIds.isNotEmpty()) { 263 | physicalCameraTv.text = physicalCameraIds 264 | .asSequence() 265 | .map { s -> "[$s]" } 266 | .reduce { acc, s -> "$acc,$s" } 267 | } 268 | } 269 | 270 | private fun calculateTransform(viewWidth: Int, viewHeight: Int) : Matrix { 271 | val rotation = activity!!.windowManager.defaultDisplay.rotation 272 | val matrix = Matrix() 273 | val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) 274 | val bufferRect = RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat()) 275 | val centerX = viewRect.centerX() 276 | val centerY = viewRect.centerY() 277 | if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { 278 | bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()) 279 | val scale = Math.max( 280 | viewHeight.toFloat() / previewSize.height, 281 | viewWidth.toFloat() / previewSize.width 282 | ) 283 | with(matrix) { 284 | setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL) 285 | postScale(scale, scale, centerX, centerY) 286 | postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY) 287 | } 288 | } else if (Surface.ROTATION_180 == rotation) { 289 | matrix.postRotate(180f, centerX, centerY) 290 | } 291 | return matrix 292 | } 293 | 294 | private fun configureTransform(viewWidth: Int, viewHeight: Int) { 295 | activity ?: return 296 | val rotation = activity!!.windowManager.defaultDisplay.rotation 297 | val matrix = Matrix() 298 | val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) 299 | val bufferRect = RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat()) 300 | val centerX = viewRect.centerX() 301 | val centerY = viewRect.centerY() 302 | 303 | if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { 304 | bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()) 305 | val scale = Math.max( 306 | viewHeight.toFloat() / previewSize.height, 307 | viewWidth.toFloat() / previewSize.width 308 | ) 309 | with(matrix) { 310 | setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL) 311 | postScale(scale, scale, centerX, centerY) 312 | postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY) 313 | } 314 | } else if (Surface.ROTATION_180 == rotation) { 315 | matrix.postRotate(180f, centerX, centerY) 316 | } 317 | camera1View.setTransform(matrix) 318 | } 319 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/extensions/CameraCharacteristicUtil.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample.extensions 2 | 3 | import android.graphics.ImageFormat 4 | import android.graphics.SurfaceTexture 5 | import android.hardware.camera2.CameraCharacteristics 6 | import android.hardware.camera2.params.StreamConfigurationMap 7 | import android.media.MediaRecorder 8 | import android.util.Size 9 | import com.tomoima.multicamerasample.services.CompareSizesByArea 10 | 11 | private const val MAX_PREVIEW_WIDTH = 1920 12 | private const val MAX_PREVIEW_HEIGHT = 1080 13 | 14 | fun CameraCharacteristics.isSupported( 15 | modes: CameraCharacteristics.Key, mode: Int 16 | ): Boolean { 17 | val ints = this.get(modes) ?: return false 18 | for (value in ints) { 19 | if (value == mode) { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | fun CameraCharacteristics.isAutoExposureSupported(mode: Int): Boolean = 27 | isSupported( 28 | CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES, 29 | mode 30 | ) 31 | 32 | 33 | fun CameraCharacteristics.isContinuousAutoFocusSupported(): Boolean = 34 | isSupported( 35 | CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES, 36 | CameraCharacteristics.CONTROL_AF_MODE_CONTINUOUS_PICTURE) 37 | 38 | 39 | fun CameraCharacteristics.isAutoWhiteBalanceSupported(): Boolean = 40 | isSupported( 41 | CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES, 42 | CameraCharacteristics.CONTROL_AWB_MODE_AUTO) 43 | 44 | fun CameraCharacteristics.getCaptureSize(comparator: Comparator): Size { 45 | val map: StreamConfigurationMap = 46 | get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: return Size(0, 0) 47 | return map.getOutputSizes(ImageFormat.JPEG) 48 | .asList() 49 | .maxWith(comparator) ?: Size(0, 0) 50 | } 51 | 52 | fun CameraCharacteristics.getVideoSize(aspectRatio: Float): Size { 53 | val map: StreamConfigurationMap = 54 | get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: return Size(0, 0) 55 | return chooseOutputSize(map.getOutputSizes(MediaRecorder::class.java).asList(), aspectRatio) 56 | } 57 | 58 | fun CameraCharacteristics.getPreviewSize(aspectRatio: Float): Size { 59 | val map: StreamConfigurationMap = 60 | get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: return Size(0, 0) 61 | return chooseOutputSize(map.getOutputSizes(SurfaceTexture::class.java).asList(), aspectRatio) 62 | } 63 | 64 | fun chooseOutputSize(sizes: List, aspectRatio: Float): Size { 65 | if(aspectRatio > 1.0f) { 66 | // land scape 67 | val size = sizes.firstOrNull { 68 | it.height == it.width * 9 / 16 && it.height < 1080 69 | } 70 | return size ?: sizes[0] 71 | } else { 72 | // portrait or square 73 | val potenitals = sizes.filter { it.height.toFloat() / it.width.toFloat() == aspectRatio } 74 | return if(potenitals.isNotEmpty()) { 75 | potenitals.firstOrNull { it.height == 1080 || it.height == 720 } ?: potenitals[0] 76 | } else { 77 | sizes[0] 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * Given `choices` of `Size`s supported by a camera, choose the smallest one that 84 | * is at least as large as the respective texture view size, and that is at most as large as the 85 | * respective max size, and whose aspect ratio matches with the specified value. If such size 86 | * doesn't exist, choose the largest one that is at most as large as the respective max size, 87 | * and whose aspect ratio matches with the specified value. 88 | * 89 | * @param textureViewWidth The width of the texture view relative to sensor coordinate 90 | * @param textureViewHeight The height of the texture view relative to sensor coordinate 91 | * @param maxWidth The maximum width that can be chosen 92 | * @param maxHeight The maximum height that can be chosen 93 | * @param aspectRatio The aspect ratio 94 | * @return The optimal `Size`, or an arbitrary one if none were big enough 95 | */ 96 | fun CameraCharacteristics.chooseOptimalSize(textureViewWidth: Int, 97 | textureViewHeight: Int, 98 | maxWidth: Int, 99 | maxHeight: Int, 100 | aspectRatio: Size): Size { 101 | var _maxWidth = maxWidth 102 | var _maxHeight = maxHeight 103 | 104 | if (_maxWidth > MAX_PREVIEW_WIDTH) { 105 | _maxWidth = MAX_PREVIEW_WIDTH 106 | } 107 | 108 | if (_maxHeight > MAX_PREVIEW_HEIGHT) { 109 | _maxHeight = MAX_PREVIEW_HEIGHT 110 | } 111 | 112 | val map = get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: return Size(0, 0) 113 | 114 | val choices = map.getOutputSizes(SurfaceTexture::class.java) 115 | 116 | // Collect the supported resolutions that are at least as big as the preview Surface 117 | val bigEnough = ArrayList() 118 | // Collect the supported resolutions that are smaller than the preview Surface 119 | val notBigEnough = ArrayList() 120 | val w = aspectRatio.width 121 | val h = aspectRatio.height 122 | for (option in choices) { 123 | if (option.width <= _maxWidth && 124 | option.height <= _maxHeight && 125 | option.height == option.width * h / w) { 126 | if (option.width >= textureViewWidth && option.height >= textureViewHeight) { 127 | bigEnough.add(option) 128 | } else { 129 | notBigEnough.add(option) 130 | } 131 | } 132 | } 133 | // Pick the smallest of those big enough. If there is no one big enough, pick the 134 | // largest of those not big enough. 135 | return when { 136 | bigEnough.size > 0 -> bigEnough.asSequence().sortedWith(CompareSizesByArea()).first() 137 | notBigEnough.size > 0 -> notBigEnough.asSequence().sortedWith(CompareSizesByArea()).last() 138 | else -> choices[0] 139 | } 140 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/listeners/SurfaceTextureWaiter.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample.listeners 2 | 3 | import android.graphics.SurfaceTexture 4 | import android.view.TextureView 5 | import com.tomoima.multicamerasample.models.State 6 | import com.tomoima.multicamerasample.models.SurfaceTextureInfo 7 | import com.tomoima.multicamerasample.ui.AutoFitTextureView 8 | import kotlin.coroutines.suspendCoroutine 9 | import kotlin.coroutines.resume 10 | 11 | class SurfaceTextureWaiter(private val textureView: AutoFitTextureView) { 12 | 13 | suspend fun textureIsReady(): SurfaceTextureInfo = 14 | suspendCoroutine { cont -> 15 | textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener { 16 | override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) { 17 | cont.resume(SurfaceTextureInfo(State.ON_TEXTURE_SIZE_CHANGED, width, height)) 18 | } 19 | 20 | override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) { 21 | } 22 | 23 | override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean = true 24 | 25 | override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) { 26 | cont.resume(SurfaceTextureInfo(State.ON_TEXTURE_AVAILABLE, width, height)) 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/models/CameraIdInfo.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample.models 2 | 3 | data class CameraIdInfo( 4 | val logicalCameraId: String = "", 5 | val physicalCameraIds: List = emptyList() 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/models/SurfaceTextureInfo.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample.models 2 | 3 | enum class State { 4 | ON_TEXTURE_SIZE_CHANGED, ON_TEXTURE_UPDATED, ON_TEXTURE_DESTROYED, ON_TEXTURE_AVAILABLE 5 | } 6 | 7 | data class SurfaceTextureInfo(val state: State, val width: Int = 0, val height: Int = 0) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/services/Camera.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample.services 2 | 3 | import android.graphics.ImageFormat 4 | import android.graphics.Rect 5 | import android.hardware.camera2.* 6 | import android.hardware.camera2.CameraAccessException 7 | import android.hardware.camera2.CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM 8 | import android.hardware.camera2.CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE 9 | import android.hardware.camera2.params.MeteringRectangle 10 | import android.hardware.camera2.params.OutputConfiguration 11 | import android.hardware.camera2.params.SessionConfiguration 12 | import android.media.Image 13 | import android.media.ImageReader 14 | import android.os.Handler 15 | import android.os.HandlerThread 16 | import android.support.v4.math.MathUtils.clamp 17 | import android.util.Log 18 | import android.util.SparseIntArray 19 | import android.view.OrientationEventListener 20 | import android.view.Surface 21 | import com.tomoima.multicamerasample.extensions.getCaptureSize 22 | import com.tomoima.multicamerasample.extensions.getPreviewSize 23 | import com.tomoima.multicamerasample.extensions.isAutoExposureSupported 24 | import com.tomoima.multicamerasample.extensions.isContinuousAutoFocusSupported 25 | import com.tomoima.multicamerasample.models.CameraIdInfo 26 | import java.util.concurrent.* 27 | 28 | private const val TAG = "CAMERA" 29 | 30 | private enum class State { 31 | PREVIEW, 32 | WAITING_LOCK, 33 | WAITING_PRECAPTURE, 34 | WAITING_NON_PRECAPTURE, 35 | TAKEN 36 | } 37 | 38 | val ORIENTATIONS = SparseIntArray().apply { 39 | append(Surface.ROTATION_0, 90) 40 | append(Surface.ROTATION_90, 0) 41 | append(Surface.ROTATION_180, 270) 42 | append(Surface.ROTATION_270, 180) 43 | } 44 | 45 | interface ImageHandler { 46 | fun handleImage(image: Image): Runnable 47 | } 48 | 49 | interface OnFocusListener { 50 | fun onFocusStateChanged(focusState: Int) 51 | } 52 | 53 | private const val ZOOM_SCALE = 1.00 54 | 55 | /** 56 | * Controller class that operates Non-UI Camera activity 57 | */ 58 | class Camera constructor(private val cameraManager: CameraManager) { 59 | 60 | companion object { 61 | // Make thread-safe Singleton 62 | @Volatile 63 | var instance: Camera? = null 64 | private set 65 | 66 | fun initInstance(cameraManager: CameraManager): Camera { 67 | val i = instance 68 | if (i != null) { 69 | return i 70 | } 71 | return synchronized(this) { 72 | val created = Camera(cameraManager) 73 | instance = created 74 | created 75 | } 76 | } 77 | } 78 | 79 | private val characteristics: CameraCharacteristics 80 | 81 | /** 82 | * An id for camera device 83 | */ 84 | private val cameraId: String 85 | 86 | private lateinit var physicalCameraIds: Set 87 | 88 | /** 89 | * A [Semaphore] to prevent the app from exiting before closing the camera. 90 | */ 91 | private val openLock = Semaphore(1) 92 | 93 | private var cameraDevice: CameraDevice? = null 94 | 95 | /** 96 | * An [ImageReader] that handles still image capture. 97 | */ 98 | private var imageReader: ImageReader? = null 99 | 100 | /** 101 | * A [CameraCaptureSession] for camera preview. 102 | */ 103 | private var captureSession: CameraCaptureSession? = null 104 | 105 | private var requestBuilder: CaptureRequest.Builder? = null 106 | 107 | private var focusListener: OnFocusListener? = null 108 | /** 109 | * The current state of camera state for taking pictures. 110 | * 111 | * @see .captureCallback 112 | */ 113 | private var state = State.PREVIEW 114 | private var aeMode = CaptureRequest.CONTROL_AE_MODE_ON 115 | private var preAfState: Int? = null 116 | 117 | // Zoom related variables 118 | // Zoom Crop Area 119 | private val activeArraySize: Rect 120 | // Zoom value 0.00 - 1.00 121 | private var zoomValue: Double = ZOOM_SCALE 122 | val maxZoom: Double 123 | 124 | /** 125 | * A [Handler] for running tasks in the background. 126 | */ 127 | private var backgroundHandler: Handler? = null 128 | /** 129 | * An additional thread for running tasks that shouldn't block the UI. 130 | */ 131 | private var backgroundThread: HandlerThread? = null 132 | private var surfaces: List? = null 133 | private var isClosed = true 134 | var deviceRotation: Int = 0 // Device rotation is defined by Screen Rotation 135 | 136 | init { 137 | cameraId = setUpCameraId(manager = cameraManager) 138 | characteristics = cameraManager.getCameraCharacteristics(cameraId) 139 | activeArraySize = characteristics.get(SENSOR_INFO_ACTIVE_ARRAY_SIZE) ?: Rect() 140 | maxZoom = characteristics.get(SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)?.toDouble() ?: ZOOM_SCALE 141 | calculateZoomSize(manager = cameraManager) 142 | calculateActiveArraySize(manager = cameraManager) 143 | Log.d(TAG, "CameraID($cameraId) -> maxZoom $maxZoom") 144 | } 145 | 146 | // Callbacks 147 | private val cameraStateCallback = object : CameraDevice.StateCallback() { 148 | override fun onOpened(camera: CameraDevice?) { 149 | cameraDevice = camera 150 | openLock.release() 151 | isClosed = false 152 | } 153 | 154 | override fun onClosed(camera: CameraDevice?) { 155 | isClosed = true 156 | } 157 | 158 | override fun onDisconnected(camera: CameraDevice?) { 159 | openLock.release() 160 | camera?.close() 161 | cameraDevice = null 162 | isClosed = true 163 | } 164 | 165 | override fun onError(camera: CameraDevice?, error: Int) { 166 | openLock.release() 167 | camera?.close() 168 | cameraDevice = null 169 | isClosed = true 170 | } 171 | } 172 | 173 | private val captureStateCallback = object : CameraCaptureSession.StateCallback() { 174 | override fun onConfigureFailed(session: CameraCaptureSession) { 175 | //TODO: handle error 176 | } 177 | 178 | override fun onConfigured(session: CameraCaptureSession) { 179 | // if camera is closed 180 | if (isClosed) return 181 | captureSession = session 182 | startPreview() 183 | } 184 | 185 | } 186 | 187 | private val captureCallback = object : CameraCaptureSession.CaptureCallback() { 188 | private fun process(result: CaptureResult) { 189 | when (state) { 190 | State.PREVIEW -> { 191 | val afState = result.get(CaptureResult.CONTROL_AF_STATE) ?: return 192 | if (afState == preAfState) { 193 | return 194 | } 195 | preAfState = afState 196 | focusListener?.onFocusStateChanged(afState) 197 | } 198 | 199 | State.WAITING_LOCK -> { 200 | val afState = result.get(CaptureResult.CONTROL_AF_STATE) 201 | // Auto Focus state is not ready in the first place 202 | if (afState == null) { 203 | runPreCapture() 204 | } else if (CaptureResult.CONTROL_AF_STATE_INACTIVE == afState || 205 | CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState || 206 | CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState 207 | ) { 208 | // CONTROL_AE_STATE can be null on some devices 209 | val aeState = result.get(CaptureResult.CONTROL_AE_STATE) 210 | if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { 211 | captureStillPicture() 212 | } else { 213 | runPreCapture() 214 | } 215 | } else { 216 | captureStillPicture() 217 | } 218 | } 219 | 220 | State.WAITING_PRECAPTURE -> { 221 | val aeState = result.get(CaptureResult.CONTROL_AE_STATE) 222 | if (aeState == null 223 | || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE 224 | || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED 225 | || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED 226 | ) { 227 | state = State.WAITING_NON_PRECAPTURE 228 | } 229 | } 230 | 231 | State.WAITING_NON_PRECAPTURE -> { 232 | val aeState = result.get(CaptureResult.CONTROL_AE_STATE) 233 | if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { 234 | captureStillPicture() 235 | } 236 | } 237 | else -> { 238 | } 239 | } 240 | } 241 | 242 | override fun onCaptureProgressed( 243 | session: CameraCaptureSession, 244 | request: CaptureRequest, 245 | partialResult: CaptureResult 246 | ) { 247 | process(partialResult) 248 | } 249 | 250 | override fun onCaptureCompleted( 251 | session: CameraCaptureSession, 252 | request: CaptureRequest, 253 | result: TotalCaptureResult 254 | ) { 255 | process(result) 256 | } 257 | 258 | } 259 | 260 | // Camera interfaces 261 | /** 262 | * Open camera and setup background handler 263 | */ 264 | fun open() { 265 | 266 | try { 267 | if (!openLock.tryAcquire(3L, TimeUnit.SECONDS)) { 268 | throw IllegalStateException("Camera launch failed") 269 | } 270 | 271 | if (cameraDevice != null) { 272 | openLock.release() 273 | return 274 | } 275 | 276 | startBackgroundHandler() 277 | 278 | cameraManager.openCamera(cameraId, cameraStateCallback, backgroundHandler) 279 | } catch (e: SecurityException) { 280 | 281 | } 282 | } 283 | 284 | /** 285 | * Start camera. Should be called after open() is successful 286 | */ 287 | fun start(surfaces: List) { 288 | this.surfaces = surfaces 289 | if(surfaces.size > 1) { 290 | startDualCamera(surfaces) 291 | } else { 292 | startSingleCamera(surfaces[0]) 293 | } 294 | } 295 | 296 | fun takePicture(handler: ImageHandler) { 297 | if (cameraDevice == null) { 298 | throw IllegalStateException("Camera device not ready") 299 | } 300 | 301 | if (isClosed) return 302 | imageReader?.setOnImageAvailableListener({ reader -> 303 | val image = reader.acquireNextImage() 304 | backgroundHandler?.post(handler.handleImage(image = image)) 305 | }, backgroundHandler) 306 | 307 | lockFocus() 308 | } 309 | 310 | fun close() { 311 | try { 312 | if (openLock.tryAcquire(3, TimeUnit.SECONDS)) 313 | isClosed = true 314 | captureSession?.close() 315 | captureSession = null 316 | 317 | cameraDevice?.close() 318 | cameraDevice = null 319 | 320 | surfaces?.forEach { 321 | it.release() 322 | } 323 | surfaces = null 324 | 325 | imageReader?.close() 326 | imageReader = null 327 | stopBackgroundHandler() 328 | zoomValue = ZOOM_SCALE 329 | } catch (e: InterruptedException) { 330 | Log.e(TAG, "Error closing camera $e") 331 | } finally { 332 | openLock.release() 333 | } 334 | } 335 | 336 | fun getCameraIds(): CameraIdInfo = CameraIdInfo(cameraId, physicalCameraIds.toList()) 337 | 338 | // Zooming 339 | 340 | /** Sets the digital zoom. Must be called while the preview is active. */ 341 | fun setZoom(zoomValue: Double) { 342 | if (zoomValue > maxZoom) { 343 | throw IllegalArgumentException("out of bounds zoom") 344 | } 345 | this.zoomValue = zoomValue 346 | 347 | try { 348 | captureSession?.stopRepeating() 349 | setCropRegion(requestBuilder, zoomValue) 350 | requestBuilder?.build()?.let { 351 | captureSession?.setRepeatingRequest(it, captureCallback, backgroundHandler) 352 | } 353 | } catch (e: CameraAccessException) { 354 | Log.w(TAG, e) 355 | } 356 | 357 | } 358 | 359 | /** 360 | * Focus manually 361 | * @param x touch X coordinate 362 | * @param y touch Y coordinate 363 | * @param width screen width 364 | * @param height screen height 365 | */ 366 | fun manualFocus(x: Float, y: Float, width: Int, height: Int) { 367 | // captureSession can be null with Monkey tap 368 | if (captureSession == null || cameraDevice == null) { 369 | return 370 | } 371 | try { 372 | val builder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) ?: return 373 | 374 | // TODO: set current Surface by aquiring current cameraID that's been used 375 | //builder.addTarget(surface) 376 | 377 | val rect = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) 378 | val areaSize = 200 379 | 380 | if (rect == null) { 381 | return 382 | } 383 | 384 | val right = rect.right 385 | val bottom = rect.bottom 386 | val centerX = x.toInt() 387 | val centerY = y.toInt() 388 | // Adjust the point of focus in the screen 389 | val ll = (centerX * right - areaSize) / width 390 | val rr = (centerY * bottom - areaSize) / height 391 | 392 | val focusLeft = clamp(ll, 0, right) 393 | val focusBottom = clamp(rr, 0, bottom) 394 | val newRect = Rect(focusLeft, focusBottom, focusLeft + areaSize, focusBottom + areaSize) 395 | // Adjust focus area with metering weight 396 | val meteringRectangle = MeteringRectangle(newRect, 500) 397 | builder.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(meteringRectangle)) 398 | builder.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(meteringRectangle)) 399 | builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) 400 | 401 | // Request should be repeated to maintain preview focus 402 | captureSession?.setRepeatingRequest(builder.build(), captureCallback, backgroundHandler) 403 | 404 | // Trigger Focus 405 | builder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) 406 | builder.set( 407 | CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, 408 | CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START 409 | ) 410 | captureSession?.capture(builder.build(), captureCallback, backgroundHandler) 411 | } catch (e: IllegalStateException) { 412 | } catch (e: CameraAccessException) { 413 | } 414 | } 415 | 416 | /** 417 | * Retrieves the image orientation from the specified screen rotation. 418 | * Used to calculate bitmap image rotation 419 | */ 420 | fun getImageOrientation(): Int { 421 | if (deviceRotation == OrientationEventListener.ORIENTATION_UNKNOWN) { 422 | return 0 423 | } 424 | val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 425 | // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X) 426 | // We have to take that into account and rotate JPEG properly. 427 | // For devices with orientation of 90, we simply return our mapping from ORIENTATIONS. 428 | // For devices with orientation of 270, we need to rotate the JPEG 180 degrees. 429 | return (ORIENTATIONS.get(deviceRotation) + sensorOrientation + 270) % 360 430 | } 431 | 432 | fun getCaptureSize() = characteristics.getCaptureSize(CompareSizesByArea()) 433 | 434 | fun getPreviewSize(aspectRatio: Float) = characteristics.getPreviewSize(aspectRatio) 435 | 436 | // internal methods 437 | 438 | private fun startSingleCamera(surface: Surface) { 439 | // setup camera session 440 | val size = characteristics.getCaptureSize(CompareSizesByArea()) 441 | imageReader = ImageReader.newInstance(size.width, size.height, ImageFormat.JPEG, 1) 442 | cameraDevice?.createCaptureSession( 443 | listOf(surface, imageReader?.surface), 444 | captureStateCallback, 445 | backgroundHandler 446 | ) 447 | } 448 | 449 | private fun startDualCamera(surfaces: List) { 450 | val outputConfigs = surfaces.mapIndexed { index, surface -> 451 | val physicalCameraId = physicalCameraIds.toList()[index] 452 | val config = OutputConfiguration(surface) 453 | config.setPhysicalCameraId(physicalCameraId) 454 | config 455 | } 456 | 457 | val executor = Executors.newCachedThreadPool() 458 | val sessionConfig = SessionConfiguration( 459 | SessionConfiguration.SESSION_REGULAR, 460 | outputConfigs, 461 | executor, 462 | captureStateCallback 463 | ) 464 | cameraDevice?.createCaptureSession(sessionConfig) 465 | } 466 | 467 | /** 468 | * Set up camera Id from id list 469 | */ 470 | private fun setUpCameraId(manager: CameraManager): String { 471 | for (cameraId in manager.cameraIdList) { 472 | val characteristics = manager.getCameraCharacteristics(cameraId) 473 | // Usually cameraId = 0 is logical camera, so we check that 474 | val capabilities = characteristics.get( 475 | CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES 476 | ) 477 | val isLogicalCamera = capabilities.contains( 478 | CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA 479 | ) 480 | if (isLogicalCamera) { 481 | this.physicalCameraIds = characteristics.physicalCameraIds 482 | return cameraId 483 | } 484 | } 485 | return "0" // default Camera. Logical Camera is not supported 486 | } 487 | 488 | private fun calculateZoomSize(manager: CameraManager) { 489 | physicalCameraIds.forEach { 490 | val characteristics = manager.getCameraCharacteristics(it) 491 | Log.d(TAG, "==== zoom $it ${characteristics.get(SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)?.toDouble()}") 492 | } 493 | } 494 | 495 | private fun calculateActiveArraySize(manager: CameraManager) { 496 | physicalCameraIds.forEach { 497 | val characteristics = manager.getCameraCharacteristics(it) 498 | val activeArraySize = characteristics.get(SENSOR_INFO_ACTIVE_ARRAY_SIZE) ?: Rect() 499 | Log.d(TAG, "==== width ${activeArraySize.width()} height ${activeArraySize.height()} $it") 500 | } 501 | } 502 | 503 | private fun startBackgroundHandler() { 504 | if (backgroundThread != null) return 505 | 506 | backgroundThread = HandlerThread("Camera-$cameraId").also { 507 | it.start() 508 | backgroundHandler = Handler(it.looper) 509 | } 510 | } 511 | 512 | private fun stopBackgroundHandler() { 513 | backgroundThread?.quitSafely() 514 | try { 515 | // TODO: investigate why thread does not end when join is called 516 | // backgroundThread?.join() 517 | backgroundThread = null 518 | backgroundHandler = null 519 | } catch (e: InterruptedException) { 520 | Log.e(TAG, "===== stop background error $e") 521 | } 522 | } 523 | 524 | private fun startPreview() { 525 | try { 526 | if (!openLock.tryAcquire(1L, TimeUnit.SECONDS)) return 527 | if (isClosed) return 528 | state = State.PREVIEW 529 | requestBuilder = createPreviewRequestBuilder() 530 | surfaces?.forEach { 531 | requestBuilder?.addTarget(it) 532 | } 533 | requestBuilder?.build()?.let { 534 | captureSession?.setRepeatingRequest(it, captureCallback, backgroundHandler) 535 | } 536 | } catch (e1: IllegalStateException) { 537 | 538 | } catch (e2: CameraAccessException) { 539 | 540 | } catch (e3: InterruptedException) { 541 | 542 | } finally { 543 | openLock.release() 544 | } 545 | } 546 | 547 | @Throws(CameraAccessException::class) 548 | private fun createPreviewRequestBuilder(): CaptureRequest.Builder? { 549 | val builder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) 550 | enableDefaultModes(builder) 551 | setCropRegion(builder, zoomValue); 552 | return builder 553 | } 554 | 555 | private fun enableDefaultModes(builder: CaptureRequest.Builder?) { 556 | builder?.apply { 557 | // Auto focus should be continuous for camera preview. 558 | // Use the same AE and AF modes as the preview. 559 | set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) 560 | 561 | if (characteristics.isContinuousAutoFocusSupported()) { 562 | set( 563 | CaptureRequest.CONTROL_AF_MODE, 564 | CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE 565 | ) 566 | } else { 567 | set( 568 | CaptureRequest.CONTROL_AF_MODE, 569 | CaptureRequest.CONTROL_AF_MODE_AUTO 570 | ) 571 | } 572 | 573 | if (characteristics.isAutoExposureSupported(aeMode)) { 574 | set(CaptureRequest.CONTROL_AE_MODE, aeMode) 575 | } else { 576 | set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) 577 | } 578 | 579 | set(CaptureRequest.COLOR_CORRECTION_MODE, CaptureRequest.COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY) 580 | } 581 | } 582 | 583 | /** 584 | * Lock the focus as the first step for a still image capture. 585 | */ 586 | private fun lockFocus() { 587 | try { 588 | state = State.WAITING_LOCK 589 | 590 | if (!characteristics.isContinuousAutoFocusSupported()) { 591 | // If continuous AF is not supported , start AF here 592 | requestBuilder?.set( 593 | CaptureRequest.CONTROL_AF_TRIGGER, 594 | CaptureRequest.CONTROL_AF_TRIGGER_START 595 | ) 596 | } 597 | requestBuilder?.build()?.let { 598 | captureSession?.capture(it, captureCallback, backgroundHandler) 599 | } 600 | } catch (e: CameraAccessException) { 601 | Log.e(TAG, "lockFocus $e") 602 | } 603 | } 604 | 605 | private fun runPreCapture() { 606 | try { 607 | state = State.WAITING_PRECAPTURE 608 | requestBuilder?.set( 609 | CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, 610 | CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START 611 | ) 612 | requestBuilder?.build()?.let { 613 | captureSession?.capture(it, captureCallback, backgroundHandler) 614 | } 615 | } catch (e: CameraAccessException) { 616 | Log.e(TAG, "runPreCapture $e") 617 | } 618 | } 619 | 620 | /** 621 | * Capture a still picture. This method should be called when we get a response in 622 | * [.captureCallback] from both [.lockFocus]. 623 | */ 624 | private fun captureStillPicture() { 625 | state = State.TAKEN 626 | try { 627 | // This is the CaptureRequest.Builder that we use to take a picture. 628 | val builder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) 629 | enableDefaultModes(builder) 630 | builder?.addTarget(imageReader?.surface) 631 | surfaces?.forEach { 632 | builder?.addTarget(it) 633 | } 634 | captureSession?.stopRepeating() 635 | captureSession?.capture( 636 | builder?.build(), 637 | object : CameraCaptureSession.CaptureCallback() { 638 | override fun onCaptureCompleted( 639 | session: CameraCaptureSession, 640 | request: CaptureRequest, 641 | result: TotalCaptureResult 642 | ) { 643 | // Once still picture is captured, ImageReader.OnImageAvailable gets called 644 | // You can do completion task here 645 | } 646 | }, 647 | backgroundHandler 648 | ) 649 | 650 | } catch (e: CameraAccessException) { 651 | Log.e(TAG, "captureStillPicture $e") 652 | } 653 | } 654 | 655 | 656 | private fun setCropRegion(builder: CaptureRequest.Builder?, zoom: Double) { 657 | builder?.let { 658 | Log.d(TAG, "setCropRegion(x$zoom)") 659 | val width = Math.floor(activeArraySize.width() / zoom).toInt() 660 | val left = (activeArraySize.width() - width) / 2 661 | val height = Math.floor(activeArraySize.height() / zoom).toInt() 662 | val top = (activeArraySize.height() - height) / 2 663 | Log.d(TAG, "crop region(left=$left, top=$top, right=${left + width}, bottom=${top + height}) zoom($zoom)") 664 | 665 | it.set( 666 | CaptureRequest.SCALER_CROP_REGION, 667 | Rect(left, top, left + width, top + height) 668 | ) 669 | } 670 | } 671 | 672 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/services/CompareSizesByArea.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample.services 2 | 3 | import android.util.Size 4 | import java.lang.Long.signum 5 | 6 | import java.util.Comparator 7 | 8 | /** 9 | * Compares two `Size`s based on their areas. 10 | */ 11 | internal class CompareSizesByArea : Comparator { 12 | 13 | // We cast here to ensure the multiplications won't overflow 14 | override fun compare(lhs: Size, rhs: Size) = 15 | signum(lhs.width.toLong() * lhs.height - rhs.width.toLong() * rhs.height) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/ui/AutoFitTextureView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tomoima.multicamerasample.ui 18 | 19 | import android.content.Context 20 | import android.util.AttributeSet 21 | import android.view.TextureView 22 | import android.view.View 23 | 24 | /** 25 | * A [TextureView] that can be adjusted to a specified aspect ratio. 26 | */ 27 | class AutoFitTextureView @JvmOverloads constructor( 28 | context: Context, 29 | attrs: AttributeSet? = null, 30 | defStyle: Int = 0 31 | ) : TextureView(context, attrs, defStyle) { 32 | 33 | private var ratioWidth = 0 34 | private var ratioHeight = 0 35 | 36 | /** 37 | * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio 38 | * calculated from the parameters. Note that the actual sizes of parameters don't matter, that 39 | * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result. 40 | * 41 | * @param width Relative horizontal size 42 | * @param height Relative vertical size 43 | */ 44 | fun setAspectRatio(width: Int, height: Int) { 45 | if (width < 0 || height < 0) { 46 | throw IllegalArgumentException("Size cannot be negative.") 47 | } 48 | ratioWidth = width 49 | ratioHeight = height 50 | requestLayout() 51 | } 52 | 53 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 54 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 55 | val width = View.MeasureSpec.getSize(widthMeasureSpec) 56 | val height = View.MeasureSpec.getSize(heightMeasureSpec) 57 | if (ratioWidth == 0 || ratioHeight == 0) { 58 | setMeasuredDimension(width, height) 59 | } else { 60 | if (width < height * ratioWidth / ratioHeight) { 61 | setMeasuredDimension(width, width * ratioHeight / ratioWidth) 62 | } else { 63 | setMeasuredDimension(height * ratioWidth / ratioHeight, height) 64 | } 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/ui/ConfirmationDialog.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample.ui 2 | 3 | import android.Manifest 4 | import android.app.AlertDialog 5 | import android.app.Dialog 6 | import android.os.Bundle 7 | import android.support.v4.app.DialogFragment 8 | import com.tomoima.multicamerasample.R 9 | 10 | /** 11 | * Shows OK/Cancel confirmation dialog about camera permission. 12 | */ 13 | class ConfirmationDialog : DialogFragment() { 14 | 15 | companion object { 16 | private const val VALUE_REQUEST_ID = "VALUE_REQUEST_ID" 17 | fun newInstance(requestId : Int): ConfirmationDialog { 18 | val bundle = Bundle() 19 | bundle.putInt(VALUE_REQUEST_ID, requestId) 20 | val confirmationDialog = ConfirmationDialog() 21 | confirmationDialog.arguments = bundle 22 | return confirmationDialog 23 | } 24 | } 25 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 26 | val permissionId = savedInstanceState?.getInt(VALUE_REQUEST_ID) ?: 0 27 | return AlertDialog.Builder(activity) 28 | .setMessage(R.string.request_permission) 29 | .setPositiveButton(android.R.string.ok) { _, _ -> 30 | parentFragment?.requestPermissions( 31 | arrayOf(Manifest.permission.CAMERA), 32 | permissionId 33 | ) 34 | } 35 | .setNegativeButton(android.R.string.cancel) { _, _ -> 36 | parentFragment?.activity?.finish() 37 | } 38 | .create() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/tomoima/multicamerasample/ui/ErrorDialog.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tomoima.multicamerasample.ui 18 | 19 | import android.app.AlertDialog 20 | import android.app.Dialog 21 | import android.os.Bundle 22 | import android.support.v4.app.DialogFragment 23 | 24 | /** 25 | * Shows an error message dialog. 26 | */ 27 | class ErrorDialog : DialogFragment() { 28 | 29 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 30 | val message = arguments?.getString(ARG_MESSAGE) ?: "" 31 | return AlertDialog.Builder(activity) 32 | .setMessage(message) 33 | .setPositiveButton(android.R.string.ok) { _, _ -> activity?.finish() } 34 | .create() 35 | } 36 | 37 | companion object { 38 | 39 | @JvmStatic 40 | private val ARG_MESSAGE = "message" 41 | 42 | @JvmStatic 43 | fun newInstance(message: String): ErrorDialog = ErrorDialog().apply { 44 | arguments = Bundle().apply { putString(ARG_MESSAGE, message) } 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_camera.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_camera.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 17 | 23 | 31 | 32 | 33 | 44 | 45 | 55 | 56 | 66 | 67 | 76 | 77 | 87 | 88 | 97 | 98 | 108 | 109 | 118 | 119 | 126 | 132 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_camera_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 17 | 23 | 31 | 32 | 33 | 44 | 45 | 55 | 56 | 66 | 67 | 76 | 77 | 87 | 88 | 97 | 98 | 108 | 109 | 118 | 119 | 126 | 132 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MultiCameraSample 3 | This sample needs camera permission. 4 | This device doesn\'t support Camera2 API. 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/tomoima/multicamerasample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.tomoima.multicamerasample 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /art/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/art/demo1.png -------------------------------------------------------------------------------- /art/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/art/demo2.png -------------------------------------------------------------------------------- /art/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/art/demo3.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.0' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.2.1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # Kotlin code style for this project: "official" or "obsolete": 15 | kotlin.code.style=official 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomoima525/MultiCameraSample/1cce4bf997397fbec739f8bb5a44ae4a0da7c92b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------