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.example.android.camera2video 18 19 import android.annotation.SuppressLint 20 import android.app.AlertDialog 21 import android.content.Context 22 import android.content.pm.PackageManager.PERMISSION_GRANTED 23 import android.content.res.Configuration 24 import android.graphics.Matrix 25 import android.graphics.RectF 26 import android.graphics.SurfaceTexture 27 import android.hardware.camera2.CameraAccessException 28 import android.hardware.camera2.CameraCaptureSession 29 import android.hardware.camera2.CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP 30 import android.hardware.camera2.CameraCharacteristics.SENSOR_ORIENTATION 31 import android.hardware.camera2.CameraDevice 32 import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW 33 import android.hardware.camera2.CameraDevice.TEMPLATE_RECORD 34 import android.hardware.camera2.CameraManager 35 import android.hardware.camera2.CameraMetadata 36 import android.hardware.camera2.CaptureRequest 37 import android.media.MediaRecorder 38 import android.os.Bundle 39 import android.os.Handler 40 import android.os.HandlerThread 41 import android.support.v4.app.ActivityCompat 42 import android.support.v4.app.Fragment 43 import android.support.v4.app.FragmentActivity 44 import android.support.v4.content.ContextCompat.checkSelfPermission 45 import android.util.Log 46 import android.util.Size 47 import android.util.SparseIntArray 48 import android.view.LayoutInflater 49 import android.view.Surface 50 import android.view.TextureView 51 import android.view.View 52 import android.view.ViewGroup 53 import android.widget.Button 54 import android.widget.Toast 55 import android.widget.Toast.LENGTH_SHORT 56 import java.io.IOException 57 import java.util.Collections 58 import java.util.concurrent.Semaphore 59 import java.util.concurrent.TimeUnit 60 import kotlin.collections.ArrayList 61 62 class Camera2VideoFragment : Fragment(), View.OnClickListener, 63 ActivityCompat.OnRequestPermissionsResultCallback { 64 65 private val FRAGMENT_DIALOG = "dialog" 66 private val TAG = "Camera2VideoFragment" 67 private val SENSOR_ORIENTATION_DEFAULT_DEGREES = 90 68 private val SENSOR_ORIENTATION_INVERSE_DEGREES = 270 <lambda>null69 private val DEFAULT_ORIENTATIONS = SparseIntArray().apply { 70 append(Surface.ROTATION_0, 90) 71 append(Surface.ROTATION_90, 0) 72 append(Surface.ROTATION_180, 270) 73 append(Surface.ROTATION_270, 180) 74 } <lambda>null75 private val INVERSE_ORIENTATIONS = SparseIntArray().apply { 76 append(Surface.ROTATION_0, 270) 77 append(Surface.ROTATION_90, 180) 78 append(Surface.ROTATION_180, 90) 79 append(Surface.ROTATION_270, 0) 80 } 81 82 /** 83 * [TextureView.SurfaceTextureListener] handles several lifecycle events on a 84 * [TextureView]. 85 */ 86 private val surfaceTextureListener = object : TextureView.SurfaceTextureListener { 87 onSurfaceTextureAvailablenull88 override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) { 89 openCamera(width, height) 90 } 91 onSurfaceTextureSizeChangednull92 override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { 93 configureTransform(width, height) 94 } 95 onSurfaceTextureDestroyednull96 override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture) = true 97 98 override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) = Unit 99 100 } 101 102 /** 103 * An [AutoFitTextureView] for camera preview. 104 */ 105 private lateinit var textureView: AutoFitTextureView 106 107 /** 108 * Button to record video 109 */ 110 private lateinit var videoButton: Button 111 112 /** 113 * A reference to the opened [android.hardware.camera2.CameraDevice]. 114 */ 115 private var cameraDevice: CameraDevice? = null 116 117 /** 118 * A reference to the current [android.hardware.camera2.CameraCaptureSession] for 119 * preview. 120 */ 121 private var captureSession: CameraCaptureSession? = null 122 123 /** 124 * The [android.util.Size] of camera preview. 125 */ 126 private lateinit var previewSize: Size 127 128 /** 129 * The [android.util.Size] of video recording. 130 */ 131 private lateinit var videoSize: Size 132 133 /** 134 * Whether the app is recording video now 135 */ 136 private var isRecordingVideo = false 137 138 /** 139 * An additional thread for running tasks that shouldn't block the UI. 140 */ 141 private var backgroundThread: HandlerThread? = null 142 143 /** 144 * A [Handler] for running tasks in the background. 145 */ 146 private var backgroundHandler: Handler? = null 147 148 /** 149 * A [Semaphore] to prevent the app from exiting before closing the camera. 150 */ 151 private val cameraOpenCloseLock = Semaphore(1) 152 153 /** 154 * [CaptureRequest.Builder] for the camera preview 155 */ 156 private lateinit var previewRequestBuilder: CaptureRequest.Builder 157 158 /** 159 * Orientation of the camera sensor 160 */ 161 private var sensorOrientation = 0 162 163 /** 164 * [CameraDevice.StateCallback] is called when [CameraDevice] changes its status. 165 */ 166 private val stateCallback = object : CameraDevice.StateCallback() { 167 168 override fun onOpened(cameraDevice: CameraDevice) { 169 cameraOpenCloseLock.release() 170 this@Camera2VideoFragment.cameraDevice = cameraDevice 171 startPreview() 172 configureTransform(textureView.width, textureView.height) 173 } 174 175 override fun onDisconnected(cameraDevice: CameraDevice) { 176 cameraOpenCloseLock.release() 177 cameraDevice.close() 178 this@Camera2VideoFragment.cameraDevice = null 179 } 180 181 override fun onError(cameraDevice: CameraDevice, error: Int) { 182 cameraOpenCloseLock.release() 183 cameraDevice.close() 184 this@Camera2VideoFragment.cameraDevice = null 185 activity?.finish() 186 } 187 188 } 189 190 /** 191 * Output file for video 192 */ 193 private var nextVideoAbsolutePath: String? = null 194 195 private var mediaRecorder: MediaRecorder? = null 196 onCreateViewnull197 override fun onCreateView(inflater: LayoutInflater, 198 container: ViewGroup?, 199 savedInstanceState: Bundle? 200 ): View? = inflater.inflate(R.layout.fragment_camera2_video, container, false) 201 202 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 203 textureView = view.findViewById(R.id.texture) 204 videoButton = view.findViewById<Button>(R.id.video).also { 205 it.setOnClickListener(this) 206 } 207 view.findViewById<View>(R.id.info).setOnClickListener(this) 208 } 209 onResumenull210 override fun onResume() { 211 super.onResume() 212 startBackgroundThread() 213 214 // When the screen is turned off and turned back on, the SurfaceTexture is already 215 // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open 216 // a camera and start preview from here (otherwise, we wait until the surface is ready in 217 // the SurfaceTextureListener). 218 if (textureView.isAvailable) { 219 openCamera(textureView.width, textureView.height) 220 } else { 221 textureView.surfaceTextureListener = surfaceTextureListener 222 } 223 } 224 onPausenull225 override fun onPause() { 226 closeCamera() 227 stopBackgroundThread() 228 super.onPause() 229 } 230 onClicknull231 override fun onClick(view: View) { 232 when (view.id) { 233 R.id.video -> if (isRecordingVideo) stopRecordingVideo() else startRecordingVideo() 234 R.id.info -> { 235 if (activity != null) { 236 AlertDialog.Builder(activity) 237 .setMessage(R.string.intro_message) 238 .setPositiveButton(android.R.string.ok, null) 239 .show() 240 } 241 } 242 } 243 } 244 245 /** 246 * Starts a background thread and its [Handler]. 247 */ startBackgroundThreadnull248 private fun startBackgroundThread() { 249 backgroundThread = HandlerThread("CameraBackground") 250 backgroundThread?.start() 251 backgroundHandler = Handler(backgroundThread?.looper) 252 } 253 254 /** 255 * Stops the background thread and its [Handler]. 256 */ stopBackgroundThreadnull257 private fun stopBackgroundThread() { 258 backgroundThread?.quitSafely() 259 try { 260 backgroundThread?.join() 261 backgroundThread = null 262 backgroundHandler = null 263 } catch (e: InterruptedException) { 264 Log.e(TAG, e.toString()) 265 } 266 } 267 268 /** 269 * Gets whether you should show UI with rationale for requesting permissions. 270 * 271 * @param permissions The permissions your app wants to request. 272 * @return Whether you can show permission rationale UI. 273 */ shouldShowRequestPermissionRationalenull274 private fun shouldShowRequestPermissionRationale(permissions: Array<String>) = 275 permissions.any { shouldShowRequestPermissionRationale(it) } 276 277 /** 278 * Requests permissions needed for recording video. 279 */ requestVideoPermissionsnull280 private fun requestVideoPermissions() { 281 if (shouldShowRequestPermissionRationale(VIDEO_PERMISSIONS)) { 282 ConfirmationDialog().show(childFragmentManager, FRAGMENT_DIALOG) 283 } else { 284 requestPermissions(VIDEO_PERMISSIONS, REQUEST_VIDEO_PERMISSIONS) 285 } 286 } 287 onRequestPermissionsResultnull288 override fun onRequestPermissionsResult( 289 requestCode: Int, 290 permissions: Array<String>, 291 grantResults: IntArray 292 ) { 293 if (requestCode == REQUEST_VIDEO_PERMISSIONS) { 294 if (grantResults.size == VIDEO_PERMISSIONS.size) { 295 for (result in grantResults) { 296 if (result != PERMISSION_GRANTED) { 297 ErrorDialog.newInstance(getString(R.string.permission_request)) 298 .show(childFragmentManager, FRAGMENT_DIALOG) 299 break 300 } 301 } 302 } else { 303 ErrorDialog.newInstance(getString(R.string.permission_request)) 304 .show(childFragmentManager, FRAGMENT_DIALOG) 305 } 306 } else { 307 super.onRequestPermissionsResult(requestCode, permissions, grantResults) 308 } 309 } 310 hasPermissionsGrantednull311 private fun hasPermissionsGranted(permissions: Array<String>) = 312 permissions.none { 313 checkSelfPermission((activity as FragmentActivity), it) != PERMISSION_GRANTED 314 } 315 316 /** 317 * Tries to open a [CameraDevice]. The result is listened by [stateCallback]. 318 * 319 * Lint suppression - permission is checked in [hasPermissionsGranted] 320 */ 321 @SuppressLint("MissingPermission") openCameranull322 private fun openCamera(width: Int, height: Int) { 323 if (!hasPermissionsGranted(VIDEO_PERMISSIONS)) { 324 requestVideoPermissions() 325 return 326 } 327 328 val cameraActivity = activity 329 if (cameraActivity == null || cameraActivity.isFinishing) return 330 331 val manager = cameraActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager 332 try { 333 if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { 334 throw RuntimeException("Time out waiting to lock camera opening.") 335 } 336 val cameraId = manager.cameraIdList[0] 337 338 // Choose the sizes for camera preview and video recording 339 val characteristics = manager.getCameraCharacteristics(cameraId) 340 val map = characteristics.get(SCALER_STREAM_CONFIGURATION_MAP) ?: 341 throw RuntimeException("Cannot get available preview/video sizes") 342 sensorOrientation = characteristics.get(SENSOR_ORIENTATION) 343 videoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder::class.java)) 344 previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture::class.java), 345 width, height, videoSize) 346 347 if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { 348 textureView.setAspectRatio(previewSize.width, previewSize.height) 349 } else { 350 textureView.setAspectRatio(previewSize.height, previewSize.width) 351 } 352 configureTransform(width, height) 353 mediaRecorder = MediaRecorder() 354 manager.openCamera(cameraId, stateCallback, null) 355 } catch (e: CameraAccessException) { 356 showToast("Cannot access the camera.") 357 cameraActivity.finish() 358 } catch (e: NullPointerException) { 359 // Currently an NPE is thrown when the Camera2API is used but not supported on the 360 // device this code runs. 361 ErrorDialog.newInstance(getString(R.string.camera_error)) 362 .show(childFragmentManager, FRAGMENT_DIALOG) 363 } catch (e: InterruptedException) { 364 throw RuntimeException("Interrupted while trying to lock camera opening.") 365 } 366 } 367 368 /** 369 * Close the [CameraDevice]. 370 */ closeCameranull371 private fun closeCamera() { 372 try { 373 cameraOpenCloseLock.acquire() 374 closePreviewSession() 375 cameraDevice?.close() 376 cameraDevice = null 377 mediaRecorder?.release() 378 mediaRecorder = null 379 } catch (e: InterruptedException) { 380 throw RuntimeException("Interrupted while trying to lock camera closing.", e) 381 } finally { 382 cameraOpenCloseLock.release() 383 } 384 } 385 386 /** 387 * Start the camera preview. 388 */ startPreviewnull389 private fun startPreview() { 390 if (cameraDevice == null || !textureView.isAvailable) return 391 392 try { 393 closePreviewSession() 394 val texture = textureView.surfaceTexture 395 texture.setDefaultBufferSize(previewSize.width, previewSize.height) 396 previewRequestBuilder = cameraDevice!!.createCaptureRequest(TEMPLATE_PREVIEW) 397 398 val previewSurface = Surface(texture) 399 previewRequestBuilder.addTarget(previewSurface) 400 401 cameraDevice?.createCaptureSession(listOf(previewSurface), 402 object : CameraCaptureSession.StateCallback() { 403 404 override fun onConfigured(session: CameraCaptureSession) { 405 captureSession = session 406 updatePreview() 407 } 408 409 override fun onConfigureFailed(session: CameraCaptureSession) { 410 if (activity != null) showToast("Failed") 411 } 412 }, backgroundHandler) 413 } catch (e: CameraAccessException) { 414 Log.e(TAG, e.toString()) 415 } 416 417 } 418 419 /** 420 * Update the camera preview. [startPreview] needs to be called in advance. 421 */ updatePreviewnull422 private fun updatePreview() { 423 if (cameraDevice == null) return 424 425 try { 426 setUpCaptureRequestBuilder(previewRequestBuilder) 427 HandlerThread("CameraPreview").start() 428 captureSession?.setRepeatingRequest(previewRequestBuilder.build(), 429 null, backgroundHandler) 430 } catch (e: CameraAccessException) { 431 Log.e(TAG, e.toString()) 432 } 433 434 } 435 setUpCaptureRequestBuildernull436 private fun setUpCaptureRequestBuilder(builder: CaptureRequest.Builder?) { 437 builder?.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO) 438 } 439 440 /** 441 * Configures the necessary [android.graphics.Matrix] transformation to `textureView`. 442 * This method should not to be called until the camera preview size is determined in 443 * openCamera, or until the size of `textureView` is fixed. 444 * 445 * @param viewWidth The width of `textureView` 446 * @param viewHeight The height of `textureView` 447 */ configureTransformnull448 private fun configureTransform(viewWidth: Int, viewHeight: Int) { 449 activity ?: return 450 val rotation = (activity as FragmentActivity).windowManager.defaultDisplay.rotation 451 val matrix = Matrix() 452 val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) 453 val bufferRect = RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat()) 454 val centerX = viewRect.centerX() 455 val centerY = viewRect.centerY() 456 457 if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { 458 bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()) 459 matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL) 460 val scale = Math.max( 461 viewHeight.toFloat() / previewSize.height, 462 viewWidth.toFloat() / previewSize.width) 463 with(matrix) { 464 postScale(scale, scale, centerX, centerY) 465 postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY) 466 } 467 } 468 textureView.setTransform(matrix) 469 } 470 471 @Throws(IOException::class) setUpMediaRecordernull472 private fun setUpMediaRecorder() { 473 val cameraActivity = activity ?: return 474 475 if (nextVideoAbsolutePath.isNullOrEmpty()) { 476 nextVideoAbsolutePath = getVideoFilePath(cameraActivity) 477 } 478 479 val rotation = cameraActivity.windowManager.defaultDisplay.rotation 480 when (sensorOrientation) { 481 SENSOR_ORIENTATION_DEFAULT_DEGREES -> 482 mediaRecorder?.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation)) 483 SENSOR_ORIENTATION_INVERSE_DEGREES -> 484 mediaRecorder?.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation)) 485 } 486 487 mediaRecorder?.apply { 488 setAudioSource(MediaRecorder.AudioSource.MIC) 489 setVideoSource(MediaRecorder.VideoSource.SURFACE) 490 setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) 491 setOutputFile(nextVideoAbsolutePath) 492 setVideoEncodingBitRate(10000000) 493 setVideoFrameRate(30) 494 setVideoSize(videoSize.width, videoSize.height) 495 setVideoEncoder(MediaRecorder.VideoEncoder.H264) 496 setAudioEncoder(MediaRecorder.AudioEncoder.AAC) 497 prepare() 498 } 499 } 500 getVideoFilePathnull501 private fun getVideoFilePath(context: Context?): String { 502 val filename = "${System.currentTimeMillis()}.mp4" 503 val dir = context?.getExternalFilesDir(null) 504 505 return if (dir == null) { 506 filename 507 } else { 508 "${dir.absolutePath}/$filename" 509 } 510 } 511 startRecordingVideonull512 private fun startRecordingVideo() { 513 if (cameraDevice == null || !textureView.isAvailable) return 514 515 try { 516 closePreviewSession() 517 setUpMediaRecorder() 518 val texture = textureView.surfaceTexture.apply { 519 setDefaultBufferSize(previewSize.width, previewSize.height) 520 } 521 522 // Set up Surface for camera preview and MediaRecorder 523 val previewSurface = Surface(texture) 524 val recorderSurface = mediaRecorder!!.surface 525 val surfaces = ArrayList<Surface>().apply { 526 add(previewSurface) 527 add(recorderSurface) 528 } 529 previewRequestBuilder = cameraDevice!!.createCaptureRequest(TEMPLATE_RECORD).apply { 530 addTarget(previewSurface) 531 addTarget(recorderSurface) 532 } 533 534 // Start a capture session 535 // Once the session starts, we can update the UI and start recording 536 cameraDevice?.createCaptureSession(surfaces, 537 object : CameraCaptureSession.StateCallback() { 538 539 override fun onConfigured(cameraCaptureSession: CameraCaptureSession) { 540 captureSession = cameraCaptureSession 541 updatePreview() 542 activity?.runOnUiThread { 543 videoButton.setText(R.string.stop) 544 isRecordingVideo = true 545 mediaRecorder?.start() 546 } 547 } 548 549 override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) { 550 if (activity != null) showToast("Failed") 551 } 552 }, backgroundHandler) 553 } catch (e: CameraAccessException) { 554 Log.e(TAG, e.toString()) 555 } catch (e: IOException) { 556 Log.e(TAG, e.toString()) 557 } 558 559 } 560 closePreviewSessionnull561 private fun closePreviewSession() { 562 captureSession?.close() 563 captureSession = null 564 } 565 stopRecordingVideonull566 private fun stopRecordingVideo() { 567 isRecordingVideo = false 568 videoButton.setText(R.string.record) 569 mediaRecorder?.apply { 570 stop() 571 reset() 572 } 573 574 if (activity != null) showToast("Video saved: $nextVideoAbsolutePath") 575 nextVideoAbsolutePath = null 576 startPreview() 577 } 578 showToastnull579 private fun showToast(message : String) = Toast.makeText(activity, message, LENGTH_SHORT).show() 580 581 /** 582 * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes 583 * larger than 1080p, since MediaRecorder cannot handle such a high-resolution video. 584 * 585 * @param choices The list of available sizes 586 * @return The video size 587 */ 588 private fun chooseVideoSize(choices: Array<Size>) = choices.firstOrNull { 589 it.width == it.height * 4 / 3 && it.width <= 1080 } ?: choices[choices.size - 1] 590 591 /** 592 * Given [choices] of [Size]s supported by a camera, chooses the smallest one whose 593 * width and height are at least as large as the respective requested values, and whose aspect 594 * ratio matches with the specified value. 595 * 596 * @param choices The list of sizes that the camera supports for the intended output class 597 * @param width The minimum desired width 598 * @param height The minimum desired height 599 * @param aspectRatio The aspect ratio 600 * @return The optimal [Size], or an arbitrary one if none were big enough 601 */ chooseOptimalSizenull602 private fun chooseOptimalSize( 603 choices: Array<Size>, 604 width: Int, 605 height: Int, 606 aspectRatio: Size 607 ): Size { 608 609 // Collect the supported resolutions that are at least as big as the preview Surface 610 val w = aspectRatio.width 611 val h = aspectRatio.height 612 val bigEnough = choices.filter { 613 it.height == it.width * h / w && it.width >= width && it.height >= height } 614 615 // Pick the smallest of those, assuming we found any 616 return if (bigEnough.isNotEmpty()) { 617 Collections.min(bigEnough, CompareSizesByArea()) 618 } else { 619 choices[0] 620 } 621 } 622 623 companion object { newInstancenull624 fun newInstance(): Camera2VideoFragment = Camera2VideoFragment() 625 } 626 627 } 628