| /* |
| * Copyright 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.camera2video |
| |
| import android.annotation.SuppressLint |
| import android.app.AlertDialog |
| import android.content.Context |
| import android.content.pm.PackageManager.PERMISSION_GRANTED |
| import android.content.res.Configuration |
| import android.graphics.Matrix |
| import android.graphics.RectF |
| import android.graphics.SurfaceTexture |
| import android.hardware.camera2.CameraAccessException |
| import android.hardware.camera2.CameraCaptureSession |
| import android.hardware.camera2.CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP |
| import android.hardware.camera2.CameraCharacteristics.SENSOR_ORIENTATION |
| import android.hardware.camera2.CameraDevice |
| import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW |
| import android.hardware.camera2.CameraDevice.TEMPLATE_RECORD |
| import android.hardware.camera2.CameraManager |
| import android.hardware.camera2.CameraMetadata |
| import android.hardware.camera2.CaptureRequest |
| import android.media.MediaRecorder |
| import android.os.Bundle |
| import android.os.Handler |
| import android.os.HandlerThread |
| import android.support.v4.app.ActivityCompat |
| import android.support.v4.app.Fragment |
| import android.support.v4.app.FragmentActivity |
| import android.support.v4.content.ContextCompat.checkSelfPermission |
| import android.util.Log |
| import android.util.Size |
| import android.util.SparseIntArray |
| import android.view.LayoutInflater |
| import android.view.Surface |
| import android.view.TextureView |
| import android.view.View |
| import android.view.ViewGroup |
| import android.widget.Button |
| import android.widget.Toast |
| import android.widget.Toast.LENGTH_SHORT |
| import java.io.IOException |
| import java.util.Collections |
| import java.util.concurrent.Semaphore |
| import java.util.concurrent.TimeUnit |
| import kotlin.collections.ArrayList |
| |
| class Camera2VideoFragment : Fragment(), View.OnClickListener, |
| ActivityCompat.OnRequestPermissionsResultCallback { |
| |
| private val FRAGMENT_DIALOG = "dialog" |
| private val TAG = "Camera2VideoFragment" |
| private val SENSOR_ORIENTATION_DEFAULT_DEGREES = 90 |
| private val SENSOR_ORIENTATION_INVERSE_DEGREES = 270 |
| private val DEFAULT_ORIENTATIONS = SparseIntArray().apply { |
| append(Surface.ROTATION_0, 90) |
| append(Surface.ROTATION_90, 0) |
| append(Surface.ROTATION_180, 270) |
| append(Surface.ROTATION_270, 180) |
| } |
| private val INVERSE_ORIENTATIONS = SparseIntArray().apply { |
| append(Surface.ROTATION_0, 270) |
| append(Surface.ROTATION_90, 180) |
| append(Surface.ROTATION_180, 90) |
| append(Surface.ROTATION_270, 0) |
| } |
| |
| /** |
| * [TextureView.SurfaceTextureListener] handles several lifecycle events on a |
| * [TextureView]. |
| */ |
| private val surfaceTextureListener = object : TextureView.SurfaceTextureListener { |
| |
| override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) { |
| openCamera(width, height) |
| } |
| |
| override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { |
| configureTransform(width, height) |
| } |
| |
| override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture) = true |
| |
| override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) = Unit |
| |
| } |
| |
| /** |
| * An [AutoFitTextureView] for camera preview. |
| */ |
| private lateinit var textureView: AutoFitTextureView |
| |
| /** |
| * Button to record video |
| */ |
| private lateinit var videoButton: Button |
| |
| /** |
| * A reference to the opened [android.hardware.camera2.CameraDevice]. |
| */ |
| private var cameraDevice: CameraDevice? = null |
| |
| /** |
| * A reference to the current [android.hardware.camera2.CameraCaptureSession] for |
| * preview. |
| */ |
| private var captureSession: CameraCaptureSession? = null |
| |
| /** |
| * The [android.util.Size] of camera preview. |
| */ |
| private lateinit var previewSize: Size |
| |
| /** |
| * The [android.util.Size] of video recording. |
| */ |
| private lateinit var videoSize: Size |
| |
| /** |
| * Whether the app is recording video now |
| */ |
| private var isRecordingVideo = false |
| |
| /** |
| * An additional thread for running tasks that shouldn't block the UI. |
| */ |
| private var backgroundThread: HandlerThread? = null |
| |
| /** |
| * A [Handler] for running tasks in the background. |
| */ |
| private var backgroundHandler: Handler? = null |
| |
| /** |
| * A [Semaphore] to prevent the app from exiting before closing the camera. |
| */ |
| private val cameraOpenCloseLock = Semaphore(1) |
| |
| /** |
| * [CaptureRequest.Builder] for the camera preview |
| */ |
| private lateinit var previewRequestBuilder: CaptureRequest.Builder |
| |
| /** |
| * Orientation of the camera sensor |
| */ |
| private var sensorOrientation = 0 |
| |
| /** |
| * [CameraDevice.StateCallback] is called when [CameraDevice] changes its status. |
| */ |
| private val stateCallback = object : CameraDevice.StateCallback() { |
| |
| override fun onOpened(cameraDevice: CameraDevice) { |
| cameraOpenCloseLock.release() |
| this@Camera2VideoFragment.cameraDevice = cameraDevice |
| startPreview() |
| configureTransform(textureView.width, textureView.height) |
| } |
| |
| override fun onDisconnected(cameraDevice: CameraDevice) { |
| cameraOpenCloseLock.release() |
| cameraDevice.close() |
| this@Camera2VideoFragment.cameraDevice = null |
| } |
| |
| override fun onError(cameraDevice: CameraDevice, error: Int) { |
| cameraOpenCloseLock.release() |
| cameraDevice.close() |
| this@Camera2VideoFragment.cameraDevice = null |
| activity?.finish() |
| } |
| |
| } |
| |
| /** |
| * Output file for video |
| */ |
| private var nextVideoAbsolutePath: String? = null |
| |
| private var mediaRecorder: MediaRecorder? = null |
| |
| override fun onCreateView(inflater: LayoutInflater, |
| container: ViewGroup?, |
| savedInstanceState: Bundle? |
| ): View? = inflater.inflate(R.layout.fragment_camera2_video, container, false) |
| |
| override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| textureView = view.findViewById(R.id.texture) |
| videoButton = view.findViewById<Button>(R.id.video).also { |
| it.setOnClickListener(this) |
| } |
| view.findViewById<View>(R.id.info).setOnClickListener(this) |
| } |
| |
| override fun onResume() { |
| super.onResume() |
| startBackgroundThread() |
| |
| // When the screen is turned off and turned back on, the SurfaceTexture is already |
| // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open |
| // a camera and start preview from here (otherwise, we wait until the surface is ready in |
| // the SurfaceTextureListener). |
| if (textureView.isAvailable) { |
| openCamera(textureView.width, textureView.height) |
| } else { |
| textureView.surfaceTextureListener = surfaceTextureListener |
| } |
| } |
| |
| override fun onPause() { |
| closeCamera() |
| stopBackgroundThread() |
| super.onPause() |
| } |
| |
| override fun onClick(view: View) { |
| when (view.id) { |
| R.id.video -> if (isRecordingVideo) stopRecordingVideo() else startRecordingVideo() |
| R.id.info -> { |
| if (activity != null) { |
| AlertDialog.Builder(activity) |
| .setMessage(R.string.intro_message) |
| .setPositiveButton(android.R.string.ok, null) |
| .show() |
| } |
| } |
| } |
| } |
| |
| /** |
| * Starts a background thread and its [Handler]. |
| */ |
| private fun startBackgroundThread() { |
| backgroundThread = HandlerThread("CameraBackground") |
| backgroundThread?.start() |
| backgroundHandler = Handler(backgroundThread?.looper) |
| } |
| |
| /** |
| * Stops the background thread and its [Handler]. |
| */ |
| private fun stopBackgroundThread() { |
| backgroundThread?.quitSafely() |
| try { |
| backgroundThread?.join() |
| backgroundThread = null |
| backgroundHandler = null |
| } catch (e: InterruptedException) { |
| Log.e(TAG, e.toString()) |
| } |
| } |
| |
| /** |
| * Gets whether you should show UI with rationale for requesting permissions. |
| * |
| * @param permissions The permissions your app wants to request. |
| * @return Whether you can show permission rationale UI. |
| */ |
| private fun shouldShowRequestPermissionRationale(permissions: Array<String>) = |
| permissions.any { shouldShowRequestPermissionRationale(it) } |
| |
| /** |
| * Requests permissions needed for recording video. |
| */ |
| private fun requestVideoPermissions() { |
| if (shouldShowRequestPermissionRationale(VIDEO_PERMISSIONS)) { |
| ConfirmationDialog().show(childFragmentManager, FRAGMENT_DIALOG) |
| } else { |
| requestPermissions(VIDEO_PERMISSIONS, REQUEST_VIDEO_PERMISSIONS) |
| } |
| } |
| |
| override fun onRequestPermissionsResult( |
| requestCode: Int, |
| permissions: Array<String>, |
| grantResults: IntArray |
| ) { |
| if (requestCode == REQUEST_VIDEO_PERMISSIONS) { |
| if (grantResults.size == VIDEO_PERMISSIONS.size) { |
| for (result in grantResults) { |
| if (result != PERMISSION_GRANTED) { |
| ErrorDialog.newInstance(getString(R.string.permission_request)) |
| .show(childFragmentManager, FRAGMENT_DIALOG) |
| break |
| } |
| } |
| } else { |
| ErrorDialog.newInstance(getString(R.string.permission_request)) |
| .show(childFragmentManager, FRAGMENT_DIALOG) |
| } |
| } else { |
| super.onRequestPermissionsResult(requestCode, permissions, grantResults) |
| } |
| } |
| |
| private fun hasPermissionsGranted(permissions: Array<String>) = |
| permissions.none { |
| checkSelfPermission((activity as FragmentActivity), it) != PERMISSION_GRANTED |
| } |
| |
| /** |
| * Tries to open a [CameraDevice]. The result is listened by [stateCallback]. |
| * |
| * Lint suppression - permission is checked in [hasPermissionsGranted] |
| */ |
| @SuppressLint("MissingPermission") |
| private fun openCamera(width: Int, height: Int) { |
| if (!hasPermissionsGranted(VIDEO_PERMISSIONS)) { |
| requestVideoPermissions() |
| return |
| } |
| |
| val cameraActivity = activity |
| if (cameraActivity == null || cameraActivity.isFinishing) return |
| |
| val manager = cameraActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager |
| try { |
| if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { |
| throw RuntimeException("Time out waiting to lock camera opening.") |
| } |
| val cameraId = manager.cameraIdList[0] |
| |
| // Choose the sizes for camera preview and video recording |
| val characteristics = manager.getCameraCharacteristics(cameraId) |
| val map = characteristics.get(SCALER_STREAM_CONFIGURATION_MAP) ?: |
| throw RuntimeException("Cannot get available preview/video sizes") |
| sensorOrientation = characteristics.get(SENSOR_ORIENTATION) |
| videoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder::class.java)) |
| previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture::class.java), |
| width, height, videoSize) |
| |
| if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { |
| textureView.setAspectRatio(previewSize.width, previewSize.height) |
| } else { |
| textureView.setAspectRatio(previewSize.height, previewSize.width) |
| } |
| configureTransform(width, height) |
| mediaRecorder = MediaRecorder() |
| manager.openCamera(cameraId, stateCallback, null) |
| } catch (e: CameraAccessException) { |
| showToast("Cannot access the camera.") |
| cameraActivity.finish() |
| } catch (e: NullPointerException) { |
| // Currently an NPE is thrown when the Camera2API is used but not supported on the |
| // device this code runs. |
| ErrorDialog.newInstance(getString(R.string.camera_error)) |
| .show(childFragmentManager, FRAGMENT_DIALOG) |
| } catch (e: InterruptedException) { |
| throw RuntimeException("Interrupted while trying to lock camera opening.") |
| } |
| } |
| |
| /** |
| * Close the [CameraDevice]. |
| */ |
| private fun closeCamera() { |
| try { |
| cameraOpenCloseLock.acquire() |
| closePreviewSession() |
| cameraDevice?.close() |
| cameraDevice = null |
| mediaRecorder?.release() |
| mediaRecorder = null |
| } catch (e: InterruptedException) { |
| throw RuntimeException("Interrupted while trying to lock camera closing.", e) |
| } finally { |
| cameraOpenCloseLock.release() |
| } |
| } |
| |
| /** |
| * Start the camera preview. |
| */ |
| private fun startPreview() { |
| if (cameraDevice == null || !textureView.isAvailable) return |
| |
| try { |
| closePreviewSession() |
| val texture = textureView.surfaceTexture |
| texture.setDefaultBufferSize(previewSize.width, previewSize.height) |
| previewRequestBuilder = cameraDevice!!.createCaptureRequest(TEMPLATE_PREVIEW) |
| |
| val previewSurface = Surface(texture) |
| previewRequestBuilder.addTarget(previewSurface) |
| |
| cameraDevice?.createCaptureSession(listOf(previewSurface), |
| object : CameraCaptureSession.StateCallback() { |
| |
| override fun onConfigured(session: CameraCaptureSession) { |
| captureSession = session |
| updatePreview() |
| } |
| |
| override fun onConfigureFailed(session: CameraCaptureSession) { |
| if (activity != null) showToast("Failed") |
| } |
| }, backgroundHandler) |
| } catch (e: CameraAccessException) { |
| Log.e(TAG, e.toString()) |
| } |
| |
| } |
| |
| /** |
| * Update the camera preview. [startPreview] needs to be called in advance. |
| */ |
| private fun updatePreview() { |
| if (cameraDevice == null) return |
| |
| try { |
| setUpCaptureRequestBuilder(previewRequestBuilder) |
| HandlerThread("CameraPreview").start() |
| captureSession?.setRepeatingRequest(previewRequestBuilder.build(), |
| null, backgroundHandler) |
| } catch (e: CameraAccessException) { |
| Log.e(TAG, e.toString()) |
| } |
| |
| } |
| |
| private fun setUpCaptureRequestBuilder(builder: CaptureRequest.Builder?) { |
| builder?.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO) |
| } |
| |
| /** |
| * Configures the necessary [android.graphics.Matrix] transformation to `textureView`. |
| * This method should not to be called until the camera preview size is determined in |
| * openCamera, or until the size of `textureView` is fixed. |
| * |
| * @param viewWidth The width of `textureView` |
| * @param viewHeight The height of `textureView` |
| */ |
| private fun configureTransform(viewWidth: Int, viewHeight: Int) { |
| activity ?: return |
| val rotation = (activity as FragmentActivity).windowManager.defaultDisplay.rotation |
| val matrix = Matrix() |
| val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat()) |
| val bufferRect = RectF(0f, 0f, previewSize.height.toFloat(), previewSize.width.toFloat()) |
| val centerX = viewRect.centerX() |
| val centerY = viewRect.centerY() |
| |
| if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { |
| bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()) |
| matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL) |
| val scale = Math.max( |
| viewHeight.toFloat() / previewSize.height, |
| viewWidth.toFloat() / previewSize.width) |
| with(matrix) { |
| postScale(scale, scale, centerX, centerY) |
| postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY) |
| } |
| } |
| textureView.setTransform(matrix) |
| } |
| |
| @Throws(IOException::class) |
| private fun setUpMediaRecorder() { |
| val cameraActivity = activity ?: return |
| |
| if (nextVideoAbsolutePath.isNullOrEmpty()) { |
| nextVideoAbsolutePath = getVideoFilePath(cameraActivity) |
| } |
| |
| val rotation = cameraActivity.windowManager.defaultDisplay.rotation |
| when (sensorOrientation) { |
| SENSOR_ORIENTATION_DEFAULT_DEGREES -> |
| mediaRecorder?.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation)) |
| SENSOR_ORIENTATION_INVERSE_DEGREES -> |
| mediaRecorder?.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation)) |
| } |
| |
| mediaRecorder?.apply { |
| setAudioSource(MediaRecorder.AudioSource.MIC) |
| setVideoSource(MediaRecorder.VideoSource.SURFACE) |
| setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) |
| setOutputFile(nextVideoAbsolutePath) |
| setVideoEncodingBitRate(10000000) |
| setVideoFrameRate(30) |
| setVideoSize(videoSize.width, videoSize.height) |
| setVideoEncoder(MediaRecorder.VideoEncoder.H264) |
| setAudioEncoder(MediaRecorder.AudioEncoder.AAC) |
| prepare() |
| } |
| } |
| |
| private fun getVideoFilePath(context: Context?): String { |
| val filename = "${System.currentTimeMillis()}.mp4" |
| val dir = context?.getExternalFilesDir(null) |
| |
| return if (dir == null) { |
| filename |
| } else { |
| "${dir.absolutePath}/$filename" |
| } |
| } |
| |
| private fun startRecordingVideo() { |
| if (cameraDevice == null || !textureView.isAvailable) return |
| |
| try { |
| closePreviewSession() |
| setUpMediaRecorder() |
| val texture = textureView.surfaceTexture.apply { |
| setDefaultBufferSize(previewSize.width, previewSize.height) |
| } |
| |
| // Set up Surface for camera preview and MediaRecorder |
| val previewSurface = Surface(texture) |
| val recorderSurface = mediaRecorder!!.surface |
| val surfaces = ArrayList<Surface>().apply { |
| add(previewSurface) |
| add(recorderSurface) |
| } |
| previewRequestBuilder = cameraDevice!!.createCaptureRequest(TEMPLATE_RECORD).apply { |
| addTarget(previewSurface) |
| addTarget(recorderSurface) |
| } |
| |
| // Start a capture session |
| // Once the session starts, we can update the UI and start recording |
| cameraDevice?.createCaptureSession(surfaces, |
| object : CameraCaptureSession.StateCallback() { |
| |
| override fun onConfigured(cameraCaptureSession: CameraCaptureSession) { |
| captureSession = cameraCaptureSession |
| updatePreview() |
| activity?.runOnUiThread { |
| videoButton.setText(R.string.stop) |
| isRecordingVideo = true |
| mediaRecorder?.start() |
| } |
| } |
| |
| override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) { |
| if (activity != null) showToast("Failed") |
| } |
| }, backgroundHandler) |
| } catch (e: CameraAccessException) { |
| Log.e(TAG, e.toString()) |
| } catch (e: IOException) { |
| Log.e(TAG, e.toString()) |
| } |
| |
| } |
| |
| private fun closePreviewSession() { |
| captureSession?.close() |
| captureSession = null |
| } |
| |
| private fun stopRecordingVideo() { |
| isRecordingVideo = false |
| videoButton.setText(R.string.record) |
| mediaRecorder?.apply { |
| stop() |
| reset() |
| } |
| |
| if (activity != null) showToast("Video saved: $nextVideoAbsolutePath") |
| nextVideoAbsolutePath = null |
| startPreview() |
| } |
| |
| private fun showToast(message : String) = Toast.makeText(activity, message, LENGTH_SHORT).show() |
| |
| /** |
| * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes |
| * larger than 1080p, since MediaRecorder cannot handle such a high-resolution video. |
| * |
| * @param choices The list of available sizes |
| * @return The video size |
| */ |
| private fun chooseVideoSize(choices: Array<Size>) = choices.firstOrNull { |
| it.width == it.height * 4 / 3 && it.width <= 1080 } ?: choices[choices.size - 1] |
| |
| /** |
| * Given [choices] of [Size]s supported by a camera, chooses the smallest one whose |
| * width and height are at least as large as the respective requested values, and whose aspect |
| * ratio matches with the specified value. |
| * |
| * @param choices The list of sizes that the camera supports for the intended output class |
| * @param width The minimum desired width |
| * @param height The minimum desired height |
| * @param aspectRatio The aspect ratio |
| * @return The optimal [Size], or an arbitrary one if none were big enough |
| */ |
| private fun chooseOptimalSize( |
| choices: Array<Size>, |
| width: Int, |
| height: Int, |
| aspectRatio: Size |
| ): Size { |
| |
| // Collect the supported resolutions that are at least as big as the preview Surface |
| val w = aspectRatio.width |
| val h = aspectRatio.height |
| val bigEnough = choices.filter { |
| it.height == it.width * h / w && it.width >= width && it.height >= height } |
| |
| // Pick the smallest of those, assuming we found any |
| return if (bigEnough.isNotEmpty()) { |
| Collections.min(bigEnough, CompareSizesByArea()) |
| } else { |
| choices[0] |
| } |
| } |
| |
| companion object { |
| fun newInstance(): Camera2VideoFragment = Camera2VideoFragment() |
| } |
| |
| } |