TL;DR – For an application that switches between front and back cameras, the recommendation is to switch between the first rear camera and the first front camera in the list of supported camera devices.
https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
For quite some time we had a bug in our application where some devices recorded nice videos, while others did not. The difference in quality between the good videos and the bad videos was so huge that it just didn’t make sense, so we dug in!
It was a dark and stormy night (not true) when we sat in a room and started discussing how we are going to fix the recording problem. We tried brainstorming ideas, but none of the ideas seemed to make sense as to why videos were good in some devices, and not others, which meant our configurations were correct, but something was off somewhere.
So we decided to create a 3-day spike for one of us in the team to dig into our camera code and try to figure it out. To add to the problem, this was happening on both camera 1 and camera 2 devices.
I won’t tire you with the boring, and mind-numbing debugging process that went into finding this small but crucial bug, but after a few days of digging in, and pretty much almost rewriting our entire camera code, I figured out the bug was related to how we opened the camera 🤦♂️.
Here is the code with the bug
Note: Code was slightly modified for this post
private void getCameraIds() {
cameraManager = (CameraManager) appContext.getSystemService(Context.CAMERA_SERVICE);
CameraCharacteristics cameraCharacteristics;
try {
for (String cameraId : cameraManager.getCameraIdList()) {
cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
if (isCameraFacingBack(cameraCharacteristics)) {
backCameraId = cameraId;
} else if (isCameraFacingFront(cameraCharacteristics)) {
frontCameraId = cameraId;
}
}
} catch (CameraAccessException exception) {
...
}
}
Before taking a look at the fix, really look at this piece of code and try to spot the bug
Here is the code with the fix
Note: Code was slightly modified for this post
private void getCameraIds() {
cameraManager = (CameraManager) appContext.getSystemService(Context.CAMERA_SERVICE);
CameraCharacteristics cameraCharacteristics;
try {
for (String cameraId : cameraManager.getCameraIdList()) {
cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
if (isCameraFacingBack(cameraCharacteristics) && backCameraId == null) {
backCameraId = cameraId;
} else if (isCameraFacingFront(cameraCharacteristics) && frontCameraId == null) {
frontCameraId = cameraId;
}
}
} catch (CameraAccessException exception) {
...
}
}
Now that you have seen the fix, do you understand why this actually fixes the bug? If you don’t, that’s ok, I will explain below.
Bug explanation
So first thing’s first, what does cameraManager.getCameraIdList()
do?
Return the list of currently connected camera devices by identifier, including cameras that may be in use by other camera API clients. Non-removable cameras use integers starting at 0 for their identifiers, while removable cameras have a unique identifier for each individual device, even if they are the same model.
developer.android.com/reference/android/hardware/camera2/CameraManager.html#getCameraIdList()
Ok, so it gives us a list of available cameras. Pretty much every example on the internet tells us to get a front-facing camera like: cameraManager.getCameraIdList()[1]
and back facing like this:cameraManager.getCameraIdList()[0]
but no one explains why, so it doesn’t really make sense. Its a list, there can be more than 2 for sure, if not there would just be a getFrontFacingCamera
and a getBackfacingCamera
instead (feels like a camera extension function to me). Well, the truth is if you do anything other then this you might find yourself in a very bad situation as we did.
In our case when it was first implemented (by whoever) this was not taken into account, maybe the original developer only had a device with 2 cameras, but no one was aware that if you used any of the other cameras in the list you might get one that is not configurable the way all of the camera examples online show that you can configure the cameras, and in many cases will not look very good because you might pick one that is not meant to be used for your use case.
So by seeing the code above, we can see that if a device had 1 back camera, and 3 front-facing camera ids (like in the case of my Pixel 3 XL) unless you add a break somewhere in the loop, you will always get the last camera of its type in the list. So in my Pixels case, I was always getting the 3rd front-facing camera – which was not the best one for our purposes.
Finding the bug
During the long debug process, after almost re-writing our camera code I actually fixed the problem but was still not 100% sure why. So I decided to keep the changes I had made and started removing and cleaning up everything else that was not needed. I had actually at some point hardcoded cameraManager.getCameraIdList()[1]
to get the front camera just until I got things working, but while doing so I didn’t realize this was the fix. So after I reverted this and used our handy looped function, I finally realized what was going on.
So I added a log inside the loop to check camera capabilities, right under cameraCharacteristics
to understand a bit more:
for (String cameraId : cameraManager.getCameraIdList()) {
cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId);
Log.e("Testing", "Camera #" + cameraId + " ===> " + Arrays.toString(cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)));
if (isCameraFacingBack(cameraCharacteristics) && backCameraId == null) {
backCameraId = cameraId;
} else if (isCameraFacingFront(cameraCharacteristics) && frontCameraId == null) {
frontCameraId = cameraId;
}
}
And to my surprise, this is what was printed out:
E/Testing: Camera #0 ===> [0, 9, 3, 7, 4, 5, 1, 6, 2] // Back facing camera
E/Testing: Camera #1 ===> [0, 3, 5, 1, 6, 2, 11] // Front facing camera 1
E/Testing: Camera #2 ===> [0, 3, 7, 4, 5, 1, 6, 2] // Front facing camera 2
E/Testing: Camera #3 ===> [0, 3, 7, 4, 5, 1, 6, 2] // Front facing camera 3
After some breakpoints I realized, that Camera #0 is ALWAYS the back-facing camera, and the other 3 are all front-facing. I then took a deeper look at the different capabilities between these 3 cameras and saw that Camera #1 had this number 11
in the list, I dug in a realized this is the constant number for the REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
capability (in the CameraMetadata), out of curiosity I read the docs for REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
and in the docs for this I read the following:
For an application that switches between front and back cameras, the recommendation is to switch between the first rear camera and the first front camera in the list of supported camera devices.
My initial thought was WTF! Why is this doc ONLY part of this capability flag? So I grabbed this doc and searched for it online to see if it might exist anywhere else, and to my surprise, again, it was nowhere to be found except on this capability flag. Oh Google…
So after digging in and reading this hidden doc, it all made sense.
Now some of you may be wondering why are there multiple front facing cameras?
Well, the reason is. that some devices use different cameras for different operations when performing more complex actions. For example, one of the cameras might be for zooming, depth estimation, etc. While other devices simply have one front-facing camera and one back-facing camera (which explains why some devices were recoding fine).
How did this affect Camera 1 API though? Well the code was pretty much copy/pasted from the Camera 2 API and the same bug was in place (Or maybe it was copied from Camera 1 API to 2 – doesn’t matter).
Conclusion
When it comes to camera code, take it with a grain of salt and be very careful with the changes being made. If your app supports both Camera 1 and 2 Apis, make sure to test both, and always test the code in at least 3 different devices at 3 different API levels, from 3 different device manufacturers. The same should be for all changes in an app in general.
I hope this helps others out there as well, and hopefully moving on with the new CameraX we get better documentation, better API, and better backward compatibility.