Fixing The Android Camera API

The other day I participated in a company hackathon and I decided to make use of the Android camera. I’ve always said that the Android APIs are very bad (to put it mildly), but I’ve never actually tried to explicitly say what is wrong and how it could be better. Until now.

So, the Camera API is crappy. If you haven’t seen it before, take a look for a minute. You can use it in a lot of wrong ways, you can forget many important things, you don’t easily find out what the problem is, even with stackoverflow, and it just doesn’t feel good. To put it differently – there is something wrong with an API that requires you to read a list of 10 steps, some of which are highlighted as “Important”. How would you write that API? Let’s improve it.

And so I did – my EasyCamera wrapper is on GitHub. Below are the changes that I made and the reason behind the change:

  1. setPreviewDisplay(..) is required before startPreview(), and it in turn is required before taking pictures. Why not enforce that? We could simply throw an exception “Preview display not set”, but that’s not good enough. Let’s get rid of the method that sets the preview display and then make startPreview(..) take the surface as parameter. Overload it to be able to take a SurfaceTexture as well.
  2. We’ve enforced the preview setting, so now let’s enforce starting the preview. We can again throw a “Preview not started” exception from takePicture(..), but that happens at runtime. Let’s instead get rid of the takePicture(..) method out of the Camera class and put it in a CameraActions class. Together with other methods that are only valid after preview is started (I don’t know which ones exactly they are – it is not clear from the current API). How do we obtain the CameraActions instance? It is returned by startPreview(..)
  3. So far we’ve made the main use-case straightforward and less error-prone by enforcing the right set of steps. But the takePicture(..) method still feels odd. You can supply nulls to all parameters, but somehow there are two overloaded methods. Arguably, passing null is fine, but there are other options. One is to introduce a PictureCallback interface that has all the four methods that can be invoked, and provide a blank implementation of all of them in a BasePictureCallback class. That, however, might not be applicable in this case, because it makes a difference if you pass null vs callback that does nothing (at least on my phone, if I pass a shutter callback, the shutter sound is played, and if I pass null, it is not). So, let’s introduce the Callbacks which is a builder-like class to contain all callbacks that you like.
  4. So far so good, but you need to restart preview after a picture is taken. And restarting it automatically may not be a good default. But in the situation we are in, you only have CameraActions and calling startPreview(..) now requires a surface to be passed. Should we introduce a restartPreview() method? No. We can make our interface methods return boolean and if the developer wants to restart preview, they should just return true. That would be fine, if there weren’t 4 different callbacks, and calculating the outcome based on all 4 is tricky. That’s why a sensible option is to add a restartPreview property to the Callbacks class, and restart only after the last callback is invoked and only if the property is set to true.
  5. The main process is improved now, but there are other things to improve. For example, there’s an asymmetry between the methods for opening and closing the camera. “open” and “release”. It should be either “open” and “close” or “acquire” and “release”. I prefer to have a .close(), and then (if you can use Java 7), make use of the AutoClosable interface, and therefore the try-with-resource construct.
  6. When you call getParameters() you can a copy of the parameters. That’s ok, but then you should set them back, and that’s counter-intuitive at first. Providing a simple camera.setParameter(..) method would be easier to work with, but the Parameters class has a lot of methods, so it’s not an option to bring them to the Camera class. We can make parameters mutable? (That isn’t implemented yet)
  7. One of the reasons the API is so cumbersome to use is the error reporting. You almost exclusively get “Came.takePicture failed”, regardless of the reason. With the above steps we eliminated the need of exceptions in some cases, but it would still be good to get better reports on the exact reason.
  8. We need to make the Camera mock-friendly (currently it isn’t). So EasyCamera is an interface, which you can easily mock. CameraActions is a simple mockable class as well.

My EasyCamera project is only an initial version now and hasn’t been used in production code, but I’d like to get feedback on whether it’s better than the original and how to get it improved. At some point it can wrap other cumbersome Camera functionality, like getting front and back camera, etc.

Broken APIs are the reason for hundreds of wasted hours, money and neurons. That’s why, when you design an API, you need some very specific skills and need to ask yourself many questions. Josh Bloch’s talk on API design is very relevant and I recommend it. I actually violated one of his advice – “if in doubt, leave it out” – you have access to the whole Camera in your CameraActions, and that might allow you to do things that don’t work. However, I am not fully aware of all features of the Camera, that’s why I didn’t want to limit users and make them invent clumsy workarounds.

When I started this post, I didn’t plan to actually write EasyCamera. But it turned out to take 2 hours, so I did it. And I would suggest that developers, whenever confronted with a bad API, do the above exercise – think of how they would’ve written it. Then it may turn out to be a lot less effort to actually fix it or wrap it, than continue using it as it is.

16 thoughts on “Fixing The Android Camera API”

  1. I’m almost finished with the initial version of a next gen OpenGL ES video engine with extensive post processing built on top of the camera API and MediaCodec… If you think the camera API is bad you’ll “love” the MediaCodec API… ;P

  2. Ok, I have the following, but it’s throwing up errors:

    public class MainActivity extends ActionBarActivity {

    EasyCamera camera = DefaultEasyCamera.open();
    CameraActions actions = camera.startPreview(surface);
    PictureCallback jpegCallback = new PictureCallback() {

    @Override
    public void onPictureTaken(byte[] data, CameraActions actions) {
    // TODO Auto-generated method stub
    actions.takePicture(Callbacks.withJpegCallback(jpegCallback));

    }

    };

    It doesn’t like the ‘surface’ in camera.startPreview(surface);

    and it’s protesting about the actions.takePicture(…) as well. Where am I going wrong?

  3. What is the “surface” that you pass? It should be a visible one, otherwise most phones complain

  4. Idea is nice, and I feel that this is a step into right direction, but library does not run in a separate thread, which still left us with a lot of complexity to deal with.

    Would be nice if library would also encapsulate this aspect as well.

  5. Hi i have tried out this great lib, but I cannot see any preview from my SurfaceView, here is the layout xml:

    and I get the SurfaceHolder from surfaceView, pass it to camera.startPreview, then the screen remains black, any idea? thx

  6. Thanks for this article. I really liked your observation: “there is something wrong with an API that requires you to read a list of 10 steps, some of which are highlighted as “Important” ”

    🙂

  7. Hi, I have the same problem as Rui my preview is black

    Here is my xml :

    Can you help me or post an example project please (it could be useful 🙂 ) ?

    Thanks

  8. How do I use the API. Can you provide some working sample?

    EasyCamera camera = DefaultEasyCamera.open();
    CameraActions actions = camera.startPreview(surface);
    PictureCallback callback = new PictureCallback() {
    public void onPictureTaken(byte[] data, CameraActions actions) {
    // store picture
    }
    };
    actions.takePicture(Callbacks.create().withJpegCallback(callback));

Leave a Reply

Your email address will not be published. Required fields are marked *