How to write an Android camera app

Published on April 15, 2011

Archived Notice

This article has been archived and may contain broken links, photos and out-of-date information. If you have any questions, please Contact Us.

Now that we've covered the internals, we can talk a little about how the surface application should look. As a reference we'll use the AOSP's own default Camera app; you can find the source here. It's a lot of code, so we'll do a lot of skipping through looking for the important stuff that you can use in your own code.

Before we start, you'll want to look at the javadoc for the Camera and MediaRecorder classes.  You'll see step-by-step instructions for using each in your application.  What follows provides context, so you can see how they are used, but ultimately, your app might look different.  So, check out the above links first and you'll know what to look for in the code snippets below.

First, a few notes about structure. The main activities are Camera and VideoCamera. The actual camera device is wrapped up in a CameraHolder class which is designed to alleviate the performance impact from having to juggle repeated calls to Camera.open() and Camera.release() as the user switches between the video and still camera activities. There is also a ImageCapture class (a subclass of Camera) which abstracts the entire process of taking a still picture and saving it locally. So an ImageCapture object represents the picture you are taking or have just finished taking.

By the way, the methods presented in the code snippets below don't necessarily appear in that order in the source code.  Here, they are arranged for clarity, with methods that call preceding the methods they are calling.

Initialization

    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.camera);
        mSurfaceView = (SurfaceView) findViewById(R.id.camera_preview);
        ...

In the onCreate() method, which is called when the activity starts, you can see that it immediately goes and finds the SurfaceView it will be posting previews to. The activity itself implements SurfaceHolder.Callback, meaning that the SurfaceHolder held by that View gets handed back to the app via the surfaceChanged() callback. The important part is that there's a reference to it for later.

Then it gets some settings from the framework, and spins off a preview thread. Inside the preview thread, the startPreview() method is called.

    private void startPreview() throws CameraHardwareException {
        if (mPausing || isFinishing()) return;
        ensureCameraDevice();
        // If we're previewing already, stop the preview first (this will blank
        // the screen).
        if (mPreviewing) stopPreview();
        setPreviewDisplay(mSurfaceHolder);
        Util.setCameraDisplayOrientation(this, mCameraId, mCameraDevice);
        setCameraParameters(UPDATE_PARAM_ALL);
        mCameraDevice.setErrorCallback(mErrorCallback);
        try {
            Log.v(TAG, "startPreview");
            mCameraDevice.startPreview();
        } catch (Throwable ex) {
            closeCamera();
            throw new RuntimeException("startPreview failed", ex);
        }
        mPreviewing = true;
        mZoomState = ZOOM_STOPPED;
        mStatus = IDLE;
    }
    private void ensureCameraDevice() throws CameraHardwareException {
        if (mCameraDevice == null) {
            mCameraDevice = CameraHolder.instance().open(mCameraId);
            mInitialParams = mCameraDevice.getParameters();
        }
    }
    private void setPreviewDisplay(SurfaceHolder holder) {
        try {
            mCameraDevice.setPreviewDisplay(holder);
        } catch (Throwable ex) {
            closeCamera();
            throw new RuntimeException("setPreviewDisplay failed", ex);
        }
    }

CameraHolder's open() method returns an instance of android.hardware.Camera, which is saved locally. It sets the Camera's preview display. Finally, it calls the device's startPreview() method (and by the way, the Camera can only take pictures once startPreview() returns successfully). At this point, the camera preview picture should show up on the app's UI. Easy!

Taking a picture

The code to actually take a picture is in the doSnap() method, called from callbacks associated with the shutter button and the UI capture button. It calls the onSnap() method of a previously instantiated ImageCapture object, which in turn calls its initiate() method, which then calls its capture() method. The capture() method sets some parameters such as orientation and GPS information (for geotagging), then finally calls the takePicture() method of the Camera object.

    private class ImageCapture {
        ...
        public void onSnap() {
            // If we are already in the middle of taking a snapshot then ignore.
            if (mPausing || mStatus == SNAPSHOT_IN_PROGRESS) {
                return;
            }
            mCaptureStartTime = System.currentTimeMillis();
            mPostViewPictureCallbackTime = 0;
            mHeadUpDisplay.setEnabled(false);
            mStatus = SNAPSHOT_IN_PROGRESS;
            mImageCapture.initiate();
        }
        public void initiate() {
            if (mCameraDevice == null) {
                return;
            }
            capture();
        }
        private void capture() {
            mCaptureOnlyData = null;
            // See android.hardware.Camera.Parameters.setRotation for
            // documentation.
            int rotation = 0;
            if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
                CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
                if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
                    rotation = (info.orientation - mOrientation + 360) % 360;
                } else {  // back-facing camera
                    rotation = (info.orientation + mOrientation) % 360;
                }
            }
            mParameters.setRotation(rotation);
            // Clear previous GPS location from the parameters.
            mParameters.removeGpsData();
            // We always encode GpsTimeStamp
            mParameters.setGpsTimestamp(System.currentTimeMillis() / 1000);
            // Set GPS location.
            Location loc = mRecordLocation ? getCurrentLocation() : null;
            if (loc != null) {
                double lat = loc.getLatitude();
                double lon = loc.getLongitude();
                boolean hasLatLon = (lat != 0.0d) || (lon != 0.0d);
                if (hasLatLon) {
                    mParameters.setGpsLatitude(lat);
                    mParameters.setGpsLongitude(lon);
                    mParameters.setGpsProcessingMethod(loc.getProvider().toUpperCase());
                    if (loc.hasAltitude()) {
                        mParameters.setGpsAltitude(loc.getAltitude());
                    } else {
                        // for NETWORK_PROVIDER location provider, we may have
                        // no altitude information, but the driver needs it, so
                        // we fake one.
                        mParameters.setGpsAltitude(0);
                    }
                    if (loc.getTime() != 0) {
                        // Location.getTime() is UTC in milliseconds.
                        // gps-timestamp is UTC in seconds.
                        long utcTimeSeconds = loc.getTime() / 1000;
                        mParameters.setGpsTimestamp(utcTimeSeconds);
                    }
                } else {
                    loc = null;
                }
            }
            mCameraDevice.setParameters(mParameters);
            mCameraDevice.takePicture(mShutterCallback, mRawPictureCallback,
                    mPostViewPictureCallback, new JpegPictureCallback(loc));
            mPreviewing = false;
        }

Notice that it passes four objects as parameters, each of which wraps a callback method as defined in the Camera class: ShutterCallback, which is called immediately after the image capture is complete; RawPictureCallback, which is called as soon as uncompressed data is available; PostViewPictureCallback, as soon as a scaled postview image (i.e., the image you see after taking the picture, which you can choose to discard or save) is available, and JpegPictureCallback, when compressed JPEG image data is available. In this case, the raw and postview callbacks only contain logging data, and the Shutter callback just tells the app that it doesn't need to auto-focus anymore.  The JpegPictureCallback, however, is important.

    private final class JpegPictureCallback implements PictureCallback {
        Location mLocation;
        public JpegPictureCallback(Location loc) {
            mLocation = loc;
        }
        public void onPictureTaken(
                final byte [] jpegData, final android.hardware.Camera camera) {
            if (mPausing) {
                return;
            }
            mJpegPictureCallbackTime = System.currentTimeMillis();
            // If postview callback has arrived, the captured image is displayed
            // in postview callback. If not, the captured image is displayed in
            // raw picture callback.
            if (mPostViewPictureCallbackTime != 0) {
                mShutterToPictureDisplayedTime =
                        mPostViewPictureCallbackTime - mShutterCallbackTime;
                mPictureDisplayedToJpegCallbackTime =
                        mJpegPictureCallbackTime - mPostViewPictureCallbackTime;
            } else {
                mShutterToPictureDisplayedTime =
                        mRawPictureCallbackTime - mShutterCallbackTime;
                mPictureDisplayedToJpegCallbackTime =
                        mJpegPictureCallbackTime - mRawPictureCallbackTime;
            }
            Log.v(TAG, "mPictureDisplayedToJpegCallbackTime = "
                    + mPictureDisplayedToJpegCallbackTime + "ms");
            mHeadUpDisplay.setEnabled(true);
            if (!mIsImageCaptureIntent) {
                // We want to show the taken picture for a while, so we wait
                // for at least 1.2 second before restarting the preview.
                long delay = 1200 - mPictureDisplayedToJpegCallbackTime;
                if (delay < 0) {
                    restartPreview();
                } else {
                    mHandler.sendEmptyMessageDelayed(RESTART_PREVIEW, delay);
                }
            }
            mImageCapture.storeImage(jpegData, camera, mLocation);
            ...
        }
    }

The Jpeg callback instructs the app to leave the postview picture on the screen for a short time, and then calls the ImageCapture.storeImage() method. This method passes all of the image data to the ImageManager class for storage, which simply writes the JPEG data straight to file. It also adds an entry for it to Android's MediaStore provider, which keeps a list of all images and other media on the device – that way it can be viewed in the Gallery right away.

Auto-focus is managed by the Camera class too. The app simply calls autoFocus() when it's ready to start focusing, and registers a callback for when focus is achieved.

    private void autoFocus() {
        // Initiate autofocus only when preview is started and snapshot is not
        // in progress.
        if (canTakePicture()) {
            mHeadUpDisplay.setEnabled(false);
            Log.v(TAG, "Start autofocus.");
            mFocusStartTime = System.currentTimeMillis();
            mFocusState = FOCUSING;
            updateFocusIndicator();
            mCameraDevice.autoFocus(mAutoFocusCallback);
        }
    }

It's also worth noting that there is a Camera.Parameters class (you probably noticed mParameters or mInitialParams in the code snippets) that bundles up all of the details, such as manual focus, zoom, antibanding, and basic photo effects.  The camera app keeps a Parameters instance locally for changes to be made easily, then passes them down to the Camera framework via Camera.setParameters().

Video capture

There's a switch on the UI that lets the user pick whether they want to capture video or still images, and it simply switches between the Camera and VideoCamera activities. So, let's look at the VideoCamera activity now.

The fundamental difference, obviously, is in what happens when you press the shutter button. This calls the startVideoRecording() method.

    private void startVideoRecording() {
        ...
        initializeRecorder();
        ...
        try {
            mMediaRecorder.start(); // Recording is now started
        } catch (RuntimeException e) {
            Log.e(TAG, "Could not start media recorder. ", e);
            releaseMediaRecorder();
            return;
        }
        ...
        keepScreenOn();
    }
    private void initializeRecorder() {
        ...
        mMediaRecorder = new MediaRecorder();
        // Unlock the camera object before passing it to media recorder.
        mCameraDevice.unlock();
        mMediaRecorder.setCamera(mCameraDevice);
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
        mMediaRecorder.setProfile(mProfile);
        mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
        // Set output file.
        if (mStorageStatus != STORAGE_STATUS_OK) {
            mMediaRecorder.setOutputFile("/dev/null");
        } else {
            // Try Uri in the intent first. If it doesn't exist, use our own
            // instead.
            if (mVideoFileDescriptor != null) {
                mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
                try {
                    mVideoFileDescriptor.close();
                } catch (IOException e) {
                    Log.e(TAG, "Fail to close fd", e);
                }
            } else {
                createVideoPath();
                mMediaRecorder.setOutputFile(mVideoFilename);
            }
        }
        mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
        ...
        try {
            mMediaRecorder.setMaxFileSize(maxFileSize);
        } catch (RuntimeException exception) {
            // We are going to ignore failure of setMaxFileSize here, as
            // a) The composer selected may simply not support it, or
            // b) The underlying media framework may not handle 64-bit range
            // on the size restriction.
        }
        // See android.hardware.Camera.Parameters.setRotation for
        // documentation.
        int rotation = 0;
        if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
            CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
            if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
                rotation = (info.orientation - mOrientation + 360) % 360;
            } else {  // back-facing camera
                rotation = (info.orientation + mOrientation) % 360;
            }
        }
        mMediaRecorder.setOrientationHint(rotation);
        mOrientationHint = rotation;
        try {
            mMediaRecorder.prepare();
        } catch (IOException e) {
            Log.e(TAG, "prepare failed for " + mVideoFilename, e);
            releaseMediaRecorder();
            throw new RuntimeException(e);
        }
        mMediaRecorder.setOnErrorListener(this);
        mMediaRecorder.setOnInfoListener(this);
    }

This first calls an initializeRecorder() method, which creates a MediaRecorder object, and sets it up for camera recording. For this to happen, it needs to know what the video and audio sources are, the details for file recording such as format and encoding, and the name of the output file. Finally, its prepare() method is called, telling it to get ready to record – if this fails, the camera cannot record video.

Once the MediaRecorder is set up, its start() method is called, and recording begins. Later on, when the activity is sent to the background or the user hits the shutter button again, the stopVideoRecording() method is called, which in turn calls the MediaRecorder's own stop() method. It then goes and registers the video with the content database, and finally releases the MediaRecorder object.

That pretty much covers the guts of your basic Camera app. There's more details, of course, but we could spend a lot of time going over every single option exposed by the Camera and MediaRecorder classes. Armed with what we've already discussed and the source code for the Camera app, you should be able to roll your own with little effort. Good luck!