Capturing Photos and Videos in Android Using Kotlin: A Guide to Camera API

Introduction

Capturing Photos and Videos in Android, With the evolution of mobile technology, camera capabilities have become one of the most critical features of smartphones. This has opened up a plethora of opportunities for developers to create rich multimedia applications. The Camera API in Android allows developers to harness these capabilities and build apps that can capture photos and videos seamlessly. In this blog, we will delve into the Camera API using Kotlin, providing extensive examples to guide you through capturing photos and videos.

Capturing Photos and Videos in Android,

Understanding Camera API

The Camera API in Android provides a set of features that allow developers to manage camera operations. The primary classes and components involved are:

  • CameraX: A Jetpack support library that makes integrating camera features easier.
  • Camera2: The advanced Camera API introduced in Android 5.0 (Lollipop) for more granular control.
  • Intents: A simpler way to use the camera for basic tasks.

Setting Up CameraX

CameraX is the preferred choice for most developers due to its ease of use and compatibility with a wide range of devices. To get started with CameraX, you need to add the necessary dependencies in your build.gradle file:

dependencies {
    def camerax_version = "1.0.1"
    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-view:$camerax_version"
    implementation "androidx.camera:camera-extensions:$camerax_version"
}

Permissions

To use the camera, your app must request the necessary permissions in the AndroidManifest.xml file:

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>

Don’t forget to check and request permissions at runtime if your app targets Android 6.0 (Marshmallow) or higher.

Capturing Photos with CameraX

Let’s dive into capturing photos with CameraX. Here’s a step-by-step guide:

  1. Layout Setup

First, create a layout file (activity_main.xml) with a PreviewView to display the camera preview and a button to capture the photo.

<androidx.camera.view.PreviewView
    android:id="@+id/viewFinder"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

<Button
    android:id="@+id/captureButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Capture"
    android:layout_gravity="center"/>
  1. Initializing CameraX

In your MainActivity.kt, initialize CameraX and set up the camera preview:

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.io.File

class MainActivity : AppCompatActivity() {

    private lateinit var viewFinder: PreviewView
    private lateinit var imageCapture: ImageCapture

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewFinder = findViewById(R.id.viewFinder)
        val captureButton: Button = findViewById(R.id.captureButton)

        // Request camera permissions
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }

        captureButton.setOnClickListener { takePhoto() }
    }

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

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

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

            imageCapture = ImageCapture.Builder().build()

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture)
            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }

    private fun takePhoto() {
        val photoFile = File(
            outputDirectory,
            SimpleDateFormat(FILENAME_FORMAT, Locale.US
            ).format(System.currentTimeMillis()) + ".jpg")

        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

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

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

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

    companion object {
        private const val TAG = "CameraXBasic"
        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
    }
}

Capturing Videos with CameraX

Capturing videos is similar to capturing photos but requires the use of VideoCapture. Here’s how you can set it up:

  1. Add VideoCapture Use Case

Modify the startCamera function to include VideoCapture.

private lateinit var videoCapture: VideoCapture

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

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

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

        imageCapture = ImageCapture.Builder().build()
        videoCapture = VideoCapture.Builder().build()

        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        try {
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(
                this, cameraSelector, preview, imageCapture, videoCapture)
        } catch(exc: Exception) {
            Log.e(TAG, "Use case binding failed", exc)
        }

    }, ContextCompat.getMainExecutor(this))
}
  1. Capture Video

Add a button to start and stop video recording. Update MainActivity.kt with the logic to handle video capture:

private lateinit var captureVideoButton: Button
private var isRecording = false

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    viewFinder = findViewById(R.id.viewFinder)
    captureVideoButton = findViewById(R.id.captureVideoButton)

    if (allPermissionsGranted()) {
        startCamera()
    } else {
        ActivityCompat.requestPermissions(
            this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
    }

    captureVideoButton.setOnClickListener {
        if (isRecording) {
            videoCapture.stopRecording()
            captureVideoButton.text = "Start Recording"
        } else {
            startRecording()
            captureVideoButton.text = "Stop Recording"
        }
        isRecording = !isRecording
    }
}

private fun startRecording() {
    val videoFile = File(
        outputDirectory,
        SimpleDateFormat(FILENAME_FORMAT, Locale.US
        ).format(System.currentTimeMillis()) + ".mp4")

    val outputOptions = VideoCapture.OutputFileOptions.Builder(videoFile).build()

    videoCapture.startRecording(
        outputOptions, ContextCompat.getMainExecutor(this), object : VideoCapture.OnVideoSavedCallback {
            override fun onVideoSaved(output: VideoCapture.OutputFileResults) {
                val savedUri = Uri.fromFile(videoFile)
                val msg = "Video capture succeeded: $savedUri"
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                Log.d(TAG, msg)
            }

            override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                Log.e(TAG, "Video capture failed: $message", cause)
            }
        })
}

Handling Permissions

Since Android 6.0 (Marshmallow), you must request permissions at runtime. Use the following method to handle permission requests:

override fun onRequestPermissionsResult(
    requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    if (requestCode == REQUEST_CODE_PERMISSIONS) {
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            Toast.makeText(this,
                "Permissions not granted by the user.",
                Toast.LENGTH_SHORT).show()
            finish()
        }
    }
}

Conclusion

The Camera API in Android, particularly through CameraX, provides a powerful and flexible way to capture photos and videos in your applications.