Camera2 API is the newer API for controlling camera devices on Android. Although it is often used in cam apps from an Activity or Fragment, there might be some use cases where you want to acquire and process camera frames in the background while other apps are in the foreground.

Just recently I started to play with Tensorflow and in my experimental app I needed to do camera frame processing in the background. Setting it up, so that frames are processed in a Service wasn't that hard. There were some differences though in comparison to regular usage. I hope this post can give some useful information to make it easier for you to accomplish the same thing.

I'll show two ways of using the camera in a Service. One way would be to still render the frames into a surface that is drawn over all other apps. This will be something like a preview window that is displayed even when other apps are in foreground. The other mode will just get the frames in the background, so that they can be processed by your app.

Depending on what you consider processing in "background", the title of this post might be a bit misleading. I used a foreground Service to process camera frames. Background service will also work, but due to background execution limits introduced in Android 8.0, it wouldn't be very useful for background processing when your app is not foreground.

You can find sample code related to this blog post on github.

I also wrote a blogpost about the native camera API on Android. If you're using the NDK and trying to access camera from C code, you might find that information useful.

Additionally, if you are interested in efficient processing of images using C/C++ and RenderScript, check out this blogpost.

Create Foreground Service

As I mentioned earlier, you could keep the service background, but because of background execution limits, the system would kill it very short time after your Activity would go to background. Therefore, I decided to go with a foreground Service and here I'll show you a quick summary. You can read my other post for some additional information about using foreground services together with OpenGL.

Because I want to show a preview of the camera while other apps are in the foreground, I need to add the SYSTEM_ALERT_WINDOW permission to my AndroidManifest.xml. This permission is required to render views over other apps. Additionally, since Android Pie (API level 28), you also need to add the FOREGROUND_SERVICE permission.

<manifest ...>

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

    <application ...>
        <service android:name=".CamService"/>
        ...

The SYSTEM_ALERT_WINDOW permission needs to be additionally granted by user in Android settings. Before you try to load a view in your foreground Service, you should first check if the user has granted this permission. You can then direct user to the settings section where he can grant this permission (Display over other app)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {

    // Don't have permission to draw over other apps yet - ask user to give permission
    val settingsIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
    startActivityForResult(settingsIntent, PERMISSION_REQUEST_CODE)
}

And here is the code that handles creating the service and starting it in foreground

class CamService: Service() {

    var rootView: View? = null
    var texPreview: TextureView? = null

    override fun onBind(p0: Intent?): IBinder? {
        return null
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

        when(intent?.action) {

            ACTION_START_WITH_PREVIEW -> startWithPreview()
        }

        return super.onStartCommand(intent, flags, startId)
    }

    override fun onCreate() {
        super.onCreate()
        startForeground()
    }

    private fun startWithPreview() {

        // Initialize view drawn over other apps
        val li = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        rootView = li.inflate(R.layout.overlay, null)
        texPreview = rootView?.findViewById(R.id.texPreview)

        val type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
            WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
        else
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

        val params = WindowManager.LayoutParams(
            type,
            WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT
        )

        val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        wm.addView(rootView, params)

        // Initialize camera here
        // ...
    }

    private fun startForeground() {

        val pendingIntent: PendingIntent =
            Intent(this, MainActivity::class.java).let { notificationIntent ->
                PendingIntent.getActivity(this, 0, notificationIntent, 0)
            }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE)
            channel.lightColor = Color.BLUE
            channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
            val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            nm.createNotificationChannel(channel)
        }

        val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle(getText(R.string.app_name))
            .setContentText(getText(R.string.app_name))
            .setSmallIcon(R.drawable.notification_template_icon_bg)
            .setContentIntent(pendingIntent)
            .setTicker(getText(R.string.app_name))
            .build()

        startForeground(ONGOING_NOTIFICATION_ID, notification)
    }

    companion object {

        val ACTION_START_WITH_PREVIEW = "eu.sisik.backgroundcam.action.START_WITH_PREVIEW"

        val ONGOING_NOTIFICATION_ID = 6660
        val CHANNEL_ID = "cam_service_channel_id"
        val CHANNEL_NAME = "cam_service_channel_name"
    }
}

The startForeground() method takes care of switching the service from background to foreground.

Once the service receives ACTION_START_WITH_PREVIEW, it calls startWithPreview() which loads the view that is rendered above other apps. You need to have the permission to draw over other apps (SYSTEM_ALERT_WINDOW) before calling wm.addView(rootView, params), or you'll get an exception.

The overlay.xml layout is very simple. It contains a TextureView which I would like to use to render the camera preview

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:app="http://schemas.android.com/apk/res-auto"
             android:layout_width="match_parent"
             android:layout_height="match_parent">

    <TextureView android:id="@+id/texPreview"
                 android:layout_width="150dp"
                 android:layout_height="150dp"
                 app:layout_constraintTop_toTopOf="parent"
                 app:layout_constraintStart_toStartOf="parent"/>

</android.support.constraint.ConstraintLayout>

At this point we should have the basic structure for starting our service and loading a view that can show our camera preview. Following sections will focus on using the Camera2 API to get camera frames for our preview and for further processing.

Camera Permissions

You need to declare the Camera permission in your AndroidManifest.xml, so that you're able to use Camera devices

<manifest ...>

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

Camera permission is considered a "dangerous permission", therefore you'll also need to request it during runtime

val permission = Manifest.permission.CAMERA
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {

    // We don't have camera permission yet... Request it from the user
    ActivityCompat.requestPermissions(this, arrayOf(permission), CODE_PERM_CAMERA)
}

This piece of code should be called from your Activity or Fragment before you start the Service.

You can then check the result of the request by overriding onRequestPermissionsResult()

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
        CODE_PERM_CAMERA -> {
            if (grantResults?.firstOrNull() != PackageManager.PERMISSION_GRANTED) {
                // Handle permission denial here
                // ...
            }
        }
    }
}

Finding Available Camera Devices

Android devices now often have more than one Camera device built in. Additionally, there can be devices connected through for example USB. In this example I just pick the first front facing camera

val manager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
var camId: String? = null

for (id in manager.cameraIdList) {
    val characteristics = manager.getCameraCharacteristics(id)
    val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
    if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
        camId = id
        break
    }
}

// Use camId to initialize camera
// ...

I this example I'm using a TextureView for showing the preview. TextureView has some lifecycle callbacks which I can use to plug in the camera initialization.

private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {

    override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
        // Init camera here
        // ...
    }

    override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {
    }

    override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
        return true
    }

    override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {}
}

...

private fun startWithPreview() {

    // Initialize view drawn over other apps
    initOverlay()

    // Initialize camera here if texture view already initialized
    if (texPreview!!.isAvailable)
        initCam(texPreview!!.width, texPreview!!.height)
    else
        texPreview!!.surfaceTextureListener = surfaceTextureListener
}

When my Service receives the request to start the preview, I first check if my TextureView is already prepared for rendering (texPreview!!.isAvailable). If the TextureView is ready, I can directly initialize the camera-related stuff. If not, I just wait till onSurfaceTextureAvailable() is called and start camera initialization there.

Determine Suitable Preview Size

A camera device will only supports a specific set of resolutions and these may vary depending on which surface is chosen as an output. In our case we want the resolutions that can be used with a TextureView. The resolutions that we get from the Camera2 api might be different from TextureView size and might have completely different aspect ratio.

// Get all supported sizes for TextureView
val characteristics = manager.getCameraCharacteristics(cameraId)
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = map.getOutputSizes(SurfaceTexture::class.java)

Once you have the supported resolutions, you can pick one that has a suitable aspect ratio and/or a resolution that is similar to your TextureView size. You can also try to adjust the TextureView size to fit a specific resolution, or you can pick from sizes that should be supported on all devices.

In our example I tried to pick aspect ration and resolution similar to the current size of our TextureView

val manager = getSystemService(Context.CAMERA_SERVICE) as CameraManager

// Get all supported sizes for TextureView
val characteristics = manager.getCameraCharacteristics(camId)
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = map.getOutputSizes(SurfaceTexture::class.java)

// We want to find something near the size of our TextureView
val texViewArea = textureViewWidth * textureViewHeight
val texViewAspect = textureViewWidth.toFloat()/textureViewHeight.toFloat()

val nearestToFurthestSz = supportedSizes.sortedWith(compareBy(
        // First find something with similar aspect
        {
            val aspect = if (it.width < it.height) it.width.toFloat() / it.height.toFloat()
            else it.height.toFloat()/it.width.toFloat()
            (aspect - texViewAspect).absoluteValue
        },
        // Also try to get similar resolution
        {
            (texViewArea - it.width * it.height).absoluteValue
        }
))

// The first entry should have similar size and aspect ratio
val mySize = nearestToFurthestSz[0]

Preparing CaptureRequest

CaptureRequest contains various capturing configuration (e.g. focusing mode, flash control, ...) and you also use it to specify target surfaces for image data.

There can be multiple target surfaces specified. In my example I decided to use 2 types of targets. I use the TextureView to display the preview. And I also created an ImageReader that can be used for background processing. If you only want to process the image data without displaying a preview, you can remove TextureView completely and only use the ImageReader.

ImageReader uses an OnImageAvailableListener to notify when a new frame is available. You need to call acquireLatestImage() or <a href""> to dequeue the image data, otherwise the the enqueued data reaches a memory limit and onImageAvailable() stops to get called

private val imageListener = object: ImageReader.OnImageAvailableListener {
    override fun onImageAvailable(reader: ImageReader?) {
        val image = reader?.acquireLatestImage()
        Log.d(TAG, "Got image " + image?.width + "x" + image?.height)

        // Process image here (ideally async, so that you don't block the callback)
        // ..

        image?.close()
    }
}

Now you can use this callback to initialize your CaptureRequest. The request builder class allows you to create a request with various parameters

// Prepare CaptureRequest that can be used with CameraCaptureSession
val requestBuilder = cameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)

val texture = textureView!!.surfaceTexture!!
texture.setDefaultBufferSize(previewSize!!.width, previewSize!!.height)
val previewSurface = Surface(texture)

requestBuilder.addTarget(previewSurface)

// Configure target surface for background processing (ImageReader)
imageReader = ImageReader.newInstance(
    previewSize!!.getWidth(), previewSize!!.getHeight(),
    ImageFormat.YUV_420_888, 2
)
imageReader!!.setOnImageAvailableListener(imageListener, null)

requestBuilder.addTarget(imageReader!!.surface)

// Set some additional parameters for the request
requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)

// We've configured a CaptureRequest 
val captureRequest = requestBuilder!!.build()

// Now use the initialized request to create a CameraCaptureSession here
// ...

In the code above I've got a 'Surface' from our TextureView and ImageReader which I've used as targets for the CaptureRequest.

Important thing to note here is that before I've created the Surface from a SurfaceTexture, I called setDefaultBufferSize() with my selected preview size. You should only use one of the supported preview sizes for SurfaceTexture that we've queried for in the previous section. The order of the dimensions is also important ( width, height != height, width). If you forget about this step, a default size supported by the camera device will be picked automatically (smallest supported size less than 1080p) and your preview might then look skewed.

I've used the imageListener from previous step to initialize ImageReader. Note that I've supplied ImageFormat.YUV_420_888 as one of the parameters to ImageReader.newInstance(). This parameter will affects the format of the pixels retrieved in onImageAvailable() and the conversions you need to do first before getting your data in usable format. I picked the YUV format because it should have guaranteed support for preview (see the tables tables starting with "LEGACY-level guaranteed configurations"). JPEG format should work too, but you would probably get much smaller frame rate due to additional compression and processing.

Preparing CameraCaptureSession

CameraCaptureSession is used to instruct the camera device to capture images.

The CaptureRequest and target surfaces that you've got in previous section of this blogpost become the main configuration parameters for creating a capture session

// Prepare CaptureRequest and target Surfaces
// ...

// Initialize a CameraCaptureSession
cameraDevice!!.createCaptureSession(listOf(previewSurface, imageReader!!.surface),
    object : CameraCaptureSession.StateCallback() {

        override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {
            // Only proceed when camera not already closed
            if (null == cameraDevice) {
                return
            }

            captureRequest = requestBuilder!!.build()
            captureSession!!.setRepeatingRequest(captureRequest!!, captureCallback, null)
        }

        override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession) {
            Log.e(TAG, "createCaptureSession()")
        }
    }, null
)

Calling setRepeatingRequest() will start capturing frames endlessly in a loop. You can then stop it by calling stopRepeating().

You can also pass a CaptureCallback to setRepeatingRequest() to get more information about the state of image capturing. Here I've only used a callback that actually does nothing. It should also be possible to just pass null.

private val captureCallback = object : CameraCaptureSession.CaptureCallback() {

    override fun onCaptureProgressed(
        session: CameraCaptureSession,
        request: CaptureRequest,
        partialResult: CaptureResult
    ) {}

    override fun onCaptureCompleted(
        session: CameraCaptureSession,
        request: CaptureRequest,
        result: TotalCaptureResult
    ) {}
}

Similar to some other methods from Camera2 API, you can pass a Handler to setRepeatingRequest() which enables you to control on which thread the CaptureCallback should be called. I only passed null in this case, so it should be invoked on the same thread setRepeatingRequest() was called on.

Opening Connection to Camera Device

Before you can actually do anything useful with a specific camera device, you need to open a connection to that device. You can do this by calling openCamera().

This again happens asynchronously and you have callback that will notify you when the connection is established. You need to wait till connection is established before you try to create and use the CameraCaptureSession from previous section

private val stateCallback = object : CameraDevice.StateCallback() {

    override fun onOpened(cameraDevice: CameraDevice) {   
        this.cameraDevice = cameraDevice

        // Wait till connection is opened and only then create the CameraCaptureSession
        createCaptureSession()
    }

    override fun onDisconnected(cameraDevice: CameraDevice) {
        cameraDevice.close()
        this.cameraDevice = null
    }

    override fun onError(cameraDevice: CameraDevice, error: Int) {
        cameraDevice.close()
        this.cameraDevice = null
    }
}

Here is how I opened the connection

cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager

// Get camera id and suitable preview size
// ...

cameraManager!!.openCamera(camId, stateCallback, null)

Closing Camera Connection & Cleanup

When using Camera2 API from regular Activity/Fragment, you would normally do the cleanup somewhere in onPause(). Service lifecycle is of course different and it all depends on your specific situation. I decided to do the cleanup in onDestroy()

...
captureSession?.close()
captureSession = null

cameraDevice?.close()
cameraDevice = null

imageReader?.close()
imageReader = null

More Useful Resources

A background service can also be used to apply effects to a video with OpengGL even without requiring the additional permissions and overlay window. I wrote another blogpost which shows how to generate a video file from captured images using the MediaCodec API inside of a background IntentService.

Additionally, I also recommend to check out my other blogpost that shows how to process images efficiently with C/C++ and RenderScript.

Next Post Previous Post

Add a comment