My goal is to be able actively process an image in the background, but when the app is in the foreground I'd need a preview which shows the processed image, meaning the original frame and bounding boxes etc. added by opencv.
So I thought I'd use a bounded foreground service which would manage JavaCamera2View from opencv (It is very possible that there is a simpler way to communicate with the activity but that's the one I found so yeah). Then the activity would bind to the service, and use the camera view, but it didn't work or at least it all went fine until I tried to call enableView on camera view from the bounded service, then app would crash. Here's the code I used(I know it's garbage and the main activity does like half of the initialization of opencv which it shouldn't):
CameraService:
class CameraService: Service() {
private val binder = CameraBinder(this)
lateinit var openCvCameraView: CameraBridgeViewBase
inner class CameraBinder (
private val context: Context,
) : Binder() {
val service = this@CameraService
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
private fun startCamera(): CameraBridgeViewBase {
val view = JavaCamera2View(this, -1)
view.visibility = SurfaceView.VISIBLE
view.setCameraPermissionGranted()
view.setCvCameraViewListener(DetectionRepository())
return view
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
Actions.START.toString() -> startService()
Actions.STOP.toString() -> stopSelf()
}
return super.onStartCommand(intent, flags, startId)
}
private fun startService() {
val notification = NotificationCompat.Builder(
this,
TestApp.NotificationChannels.PROCESSING_CAMERA.toString()
)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("Processing the camera feed")
.setContentText("some text")
.build()
openCvCameraView = startCamera()
startForeground(1, notification)
}
enum class Actions {
START,
STOP,
}
}
Main activity:
class MainActivity : ComponentActivity() {
private lateinit var cameraService: CameraService
private var cameraServiceBound: Boolean = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as CameraService.CameraBinder
cameraService = binder.service
cameraServiceBound = true
}
override fun onServiceDisconnected(arg0: ComponentName) {
cameraServiceBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
if (OpenCVLoader.initLocal()) {
Log.i("LOADED", "OpenCV loaded successfully")
} else {
Log.e("LOADED", "OpenCV initialization failed!")
(Toast.makeText(this, "OpenCV initialization failed!", Toast.LENGTH_LONG))
.show()
return
}
Intent(this, CameraService::class.java).also {
it.action = CameraService.Actions.START.toString()
startForegroundService(it)
bindService(it, connection, Context.BIND_AUTO_CREATE)
}
if (!hasRequiredPermissions()) {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, 0)
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
cameraService.openCvCameraView.setCameraPermissionGranted()
cameraService.openCvCameraView.enableView() // crashes because of this
setContent {
AppTheme {
Column {
if (cameraServiceBound) {
AndroidView(factory = { cameraService.openCvCameraView })
}
}
}
}
}
override fun onStart() {
Intent(this, CameraService::class.java).also {
it.action = CameraService.Actions.START.toString()
bindService(it, connection, Context.BIND_AUTO_CREATE)
}
super.onStart()
}
private fun hasRequiredPermissions(): Boolean {
return REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
applicationContext,
it
) == PackageManager.PERMISSION_GRANTED
}
}
companion object {
val REQUIRED_PERMISSIONS = assemblePermissions()
private fun assemblePermissions(): Array<String> {
val basePerms = mutableListOf(
Manifest.permission.CAMERA,
Manifest.permission.FOREGROUND_SERVICE,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
basePerms.add(Manifest.permission.FOREGROUND_SERVICE_CAMERA)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
basePerms.add(Manifest.permission.POST_NOTIFICATIONS)
}
return basePerms.toTypedArray()
}
}
}
One more thing I've been considering is using CameraX and just converting each frame to a Mat using something similar to the implementation of JavaCamera2Frame in opencv. But I don't know how I would then change the output of the PreviewView to use the processed frames also I'd need to somehow convert back to the format CameraX is using.
I've looked around and most I've seen some people asking how to use opencv in the background but it I haven't seen anyone making a preview out of frames processed by opencv which is what I'm struggling to achieve.
I'm new to android development and opencv in general, I don't really know how to achieve my goal in this situation.
Is there a good solution to achieve this? Is this even possible?
Update
I was experiencing crashes with JavaCamera2View because I had a race condition. I tried to use the bounded cameraService when the connection wasn't established yet. I fixed this problem by moving the ui code into onServiceConnected as a temporary fix, it looks something like this:
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as CameraService.CameraBinder
cameraService = binder.service
cameraServiceBound = true
setContent {
AppTheme {
Column {
AndroidView(factory = { cameraService.openCvCameraView })
}
}
}
}
However this still didn't solve my issue as after this fix when the app was in the background the opencv would just stop processing frames, as if I had JavaCamera2View inside of the Activity. So this path led me to nowhere.
JavaCamera2Viewnot working in a foreground service.