Captura de vídeo e imagem com CameraX no Android

Jan 31 2023
Índice Arquivo build.gradle no nível do aplicativo, permissões Implementação recomendada: geralmente, você deve exibir uma IU explicando ao usuário os recursos que serão ativados se eles concederem permissões para o aplicativo postar notificações.

Índice

  • Permissões
  • Captura de imagem
    - Salvar foto
    - Capturar foto
  • Captura de vídeo

arquivo de nível de aplicativo build.gradle,

def camerax_version = "1.3.0-alpha02"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"

implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

Permissões

class MainActivity : AppCompatActivity() {
    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS =
            mutableListOf (
                Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO,
            ).apply {
                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                    add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            }.toTypedArray()
    }

    private lateinit var viewModel: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider(this).get(SharedViewModel::class.java)

        if (!allPermissionsGranted()) {
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        } else {
            viewModel.setPermission(allPermissionsGranted())
        }

        //...
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            viewModel.setPermission(allPermissionsGranted())
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }
}

Implementação recomendada: geralmente, você deve exibir uma IU explicando ao usuário os recursos que serão ativados se ele conceder permissões para o aplicativo postar notificações. Essa interface do usuário deve fornecer ao usuário opções para concordar ou negar, como os botões OK e Não, obrigado . Se o usuário selecionar OK , solicite a permissão diretamente. Se o usuário selecionar Não, obrigado , permita que o usuário continue sem notificações.

Captura de imagem

class MainFragment : Fragment() {
    //...

    private var imageCapture: ImageCapture? = null

    private lateinit var cameraExecutor: ExecutorService

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        sharedViewModel.isPermissionGranted.observe(viewLifecycleOwner) {
            if (it) startCamera()
        }

        _binding.imageCaptureButton.setOnClickListener { capturePhoto() }
        _binding.imageSaveButton.setOnClickListener { savePhoto() }

        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun capturePhoto() {
        //...
    }

    private fun savePhoto() {
        //...
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())

        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(_binding.viewFinder.surfaceProvider)
                }

            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }
        }, ContextCompat.getMainExecutor(requireContext()))
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }
}

ProcessCameraProvider , um singleton que pode ser usado para vincular o ciclo de vida das câmeras a qualquer um LifecycleOwnerdentro do processo de um aplicativo.

Somente um único provedor de câmera de processo pode existir em um processo e pode ser recuperado com getInstance.

setCaptureMode, é configurar o modo de captura ao tirar uma foto,

  • CAPTURE_MODE_MINIMIZE_LATENCYotimizar a captura de imagem para latência.
  • CAPTURE_MODE_MAXIMIZE_QUALITYotimizar a captura de imagem para qualidade de imagem.

Agora, vamos implementar savePhotoe capturePhoto. A diferença é que savePhotosalva a imagem no local do arquivo fornecido, mas capturePhotofornece um buffer na memória da imagem capturada.

Salvar foto

private fun savePhoto() {
    // If the use case is null, exit out of the function.
    // This will be null If we tap the photo button before image capture is set up.
    // Without the return statement, the app would crash if it was null.
    val imageCapture = imageCapture ?: return

    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/PATH")
        }
    }

    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(requireActivity().contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues)
        .build()

    imageCapture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(requireContext()),
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
            }

            override fun onImageSaved(output: ImageCapture.OutputFileResults){
                val msg = "Photo capture succeeded: ${output.savedUri}"
                Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
                Log.d(TAG, msg)
            }
        }
    )
}

outputOptions, cria um objeto de opções de saída que contém arquivo e metadados.

takePicture(OutputFileOptions, Executor, OnImageSavedCallback), este método salva a imagem capturada no local do arquivo fornecido.

Capturar foto

private fun capturePhoto() {
    val imageCapture = imageCapture ?: return

    imageCapture.takePicture(
        ContextCompat.getMainExecutor(requireContext()),
        object : ImageCapture.OnImageCapturedCallback() {
            override fun onCaptureSuccess(image: ImageProxy) {
                super.onCaptureSuccess(image)
                parentFragmentManager.beginTransaction()
                    .replace(R.id.container, ImageViewFragment.newInstance(image))
                    .addToBackStack(null)
                    .commit()
            }
        }
    )
}

É isso. Passamos image: ImageProxye ImageViewFragmentexibimos para o usuário.

Observação: Para exibir ImageProxycomo Imagem, precisamos convertê-la em bitmap.

fun Image.toBitmap(): Bitmap {
    val buffer = planes[0].buffer
    buffer.rewind()
    val bytes = ByteArray(buffer.capacity())
    buffer.get(bytes)
    return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}

class VideoFragment : Fragment() {
    //...

    private var videoCapture: VideoCapture<Recorder>? = null
    private var recording: Recording? = null

    private lateinit var cameraExecutor: ExecutorService

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        sharedViewModel.isPermissionGranted.observe(viewLifecycleOwner) {
            if (it) startCamera()
        }

        _binding.videoCaptureButton.setOnClickListener { captureVideo() }

        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun captureVideo() {
        //...
    }

    private fun startCamera() {
        //...

        // Video
        val recorder = Recorder.Builder()
            .setQualitySelector(
                QualitySelector.from(
                    Quality.HIGHEST,
                    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)
                )
            )
            .build()
        videoCapture = VideoCapture.withOutput(recorder)
        
        //...
        cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }
}

startCameraé o mesmo que fizemos na parte de Captura de Imagem . A única diferença é que precisamos remover imageCapturee adicionar recordervariáveis videoCapture.

private fun captureVideo() {
    val videoCapture = this.videoCapture ?: return

    _binding.videoCaptureButton.isEnabled = false

    val curRecording = recording
    if (curRecording != null) {
        curRecording.stop()
        recording = null
        return
    }

    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
        }
    }

    val mediaStoreOutputOptions = MediaStoreOutputOptions
        .Builder(requireActivity().contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
        .setContentValues(contentValues)
        .build()

    recording = videoCapture.output
        .prepareRecording(requireContext(), mediaStoreOutputOptions)
        .apply {
            // Enable Audio for recording
            if (
                PermissionChecker.checkSelfPermission(requireContext(), Manifest.permission.RECORD_AUDIO) ==
                PermissionChecker.PERMISSION_GRANTED
            ) {
                withAudioEnabled()
            }
        }
        .start(ContextCompat.getMainExecutor(requireContext())) { recordEvent ->
            when(recordEvent) {
                is VideoRecordEvent.Start -> {
                    _binding.videoCaptureButton.apply {
                        text = getString(R.string.stop_capture)
                        isEnabled = true
                    }
                }
                is VideoRecordEvent.Finalize -> {
                    if (!recordEvent.hasError()) {
                        val msg = "Video capture succeeded: ${recordEvent.outputResults.outputUri}"
                        parentFragmentManager.beginTransaction()
                            .replace(R.id.container, VideoViewFragment.newInstance(recordEvent.outputResults.outputUri))
                            .addToBackStack(null)
                            .commit()
                        Log.d(TAG, msg)
                    } else {
                        recording?.close()
                        recording = null
                        Log.e(TAG, "Video capture ends with error: ${recordEvent.error}")
                    }
                    _binding.videoCaptureButton.apply {
                        text = getString(R.string.start_capture)
                        isEnabled = true
                    }
                }
            }
        }
}

Para iniciar a gravação, criamos uma nova sessão de gravação. Primeiro, criamos o objeto de vídeo MediaStore pretendido com carimbo de data/hora do sistema como nome de exibição.

.startinicia a gravação, tornando-a uma gravação ativa. Apenas uma única gravação pode estar ativa por vez.

  • Após o início bem-sucedido, VideoRecordEvent.Starto evento será acionado.
  • Após a gravação bem-sucedida, VideoRecordEvent.Finalizeserá acionado. Além disso, se ocorrer um erro ao iniciar a gravação, VideoRecordEvent.Finalizeo evento será o primeiro a ser acionado. Podemos acessar o erro com .getErroro método.

Código Completo

MrNtlu/Camera-Guide (github.com)

Fontes:

  • Visão geral do CameraX | Desenvolvedores Android
  • https://developer.android.com/codelabs/camerax-getting-started
  • Captura de imagem | Desenvolvedores Android
  • Arquitetura de captura de vídeo CameraX | Desenvolvedores Android
  • Seletor de fotos | Desenvolvedores Android
  • LinkedIn
  • GithubGenericName
  • Twitter