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