androidandroid-serviceandroid-camera2android-video-record

Camera2 API running in background service


I have used the Camera2 API to implement a background video recorder running as a service and recording from the front cam. For this I have created a new SurfaceView, set its size to 1x1 and moved it to the top left corner. My code is shown below. I'm using Android 5.1.

With the Camera API it worked very well but unfortunately the frame rate is only at 20 fps (and when I increase the exposure compensation it drops even more), although with the Open Camera app I have 30 fps also with increased exposure compensation. That is why I want to try Camera2 API (hoping to get higher fps). Unfortunately, I'm getting the following error:

MediaRecoder: setOutputFile called in an invalid state(2)
MediaRecorder: start called in an invalid state: 2

Here is my code:

public class RecorderServiceCamera2 extends Service implements SurfaceHolder.Callback {
    private WindowManager windowManager;
    private SurfaceView surfaceView;
    private CameraDevice mCamera;
    private MediaRecorder mediaRecorder = null;
    private CaptureRequest mCaptureRequest;
    private CameraCaptureSession mSession;

    @Override
    public void onCreate() {
        // Create new SurfaceView, set its size to 1x1, move it to the top left corner and set this service as a callback
        windowManager = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);

        surfaceView = new SurfaceView(this);
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                1, 1,
                WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
                PixelFormat.TRANSLUCENT
        );
        layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
        windowManager.addView(surfaceView, layoutParams);
        surfaceView.getHolder().addCallback(this);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Intent notificationIntent = new Intent(this, MainActivity.class);

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
                notificationIntent, 0);

        Notification notification = new NotificationCompat.Builder(this)
                //.setSmallIcon(R.mipmap.app_icon)
                .setContentTitle("Background Video Recorder")
                .setContentText("")
                .setContentIntent(pendingIntent).build();

        startForeground(MainActivity.NOTIFICATION_ID_RECORDER_SERVICE, notification);

        return Service.START_NOT_STICKY;
    }

    @Override
    public void surfaceCreated(final SurfaceHolder surfaceHolder) {
        mediaRecorder = new MediaRecorder();

        try {
            CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE);
            String[] cameras = manager.getCameraIdList();
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameras[1]);
            StreamConfigurationMap configs = characteristics.get(
                    CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            Size[] sizes = configs.getOutputSizes(MediaCodec.class);

            final Size sizeHigh = sizes[0];

            manager.openCamera(cameras[1], new CameraDevice.StateCallback() {
                @Override
                public void onOpened(@NonNull CameraDevice camera) {
                    mCamera = camera;
                    mediaRecorder.setPreviewDisplay(surfaceHolder.getSurface());
                    mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
                    mediaRecorder.setMaxFileSize(0);
                    mediaRecorder.setOrientationHint(0);

                    mediaRecorder.setOutputFile("test.mp4");
                    try { mediaRecorder.prepare(); } catch (Exception ignored) {}
                    List<Surface> list = new ArrayList<>();
                    list.add(surfaceHolder.getSurface());

                    try {
                        CaptureRequest.Builder captureRequest = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
                        captureRequest.addTarget(surfaceHolder.getSurface());
                        mCaptureRequest = captureRequest.build();
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }

                    try {
                        mCamera.createCaptureSession(list, new CameraCaptureSession.StateCallback() {
                            @Override
                            public void onConfigured(@NonNull CameraCaptureSession session) {
                                mSession = session;
                            }

                            @Override
                            public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                                mSession = session;
                            }
                        }, null);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }

                    mediaRecorder.start();

                    try {
                        mSession.setRepeatingRequest(mCaptureRequest,
                                new CameraCaptureSession.CaptureCallback() {
                                    @Override
                                    public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
                                        super.onCaptureStarted(session, request, timestamp, frameNumber);
                                    }

                                    @Override
                                    public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                                        super.onCaptureCompleted(session, request, result);
                                    }

                                    @Override
                                    public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
                                    }
                                }, null);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void onDisconnected(@NonNull CameraDevice camera) {
                }

                @Override
                public void onError(@NonNull CameraDevice camera, int error) {
                }
            }, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    // Stop recording and remove SurfaceView
    @Override
    public void onDestroy() {
        mediaRecorder.stop();
        mediaRecorder.reset();
        mediaRecorder.release();
        mCamera.close();
        mediaRecorder= null;

        windowManager.removeView(surfaceView);
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {}

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {}

    @Override
    public IBinder onBind(Intent intent) { return null; }
}

Solution

  • You don't need to put up a SurfaceView with Camera2, or even with Camera1 (with the latter, just create a SurfaceTexture to get a Surface from, and never call updateTexImage; with the former, just don't include a preview Surface at all, it's not needed).

    That said, you're trying to use the same Surface in both Camera2 and the MediaRecorder; that doesn't work. MediaRecorder doesn't need to draw to a Surface at all to record, you can just leave that part off. It's only there to allow MediaRecorder to be used independently of the camera API, where it manages a camera behind the scenes.

    I suspect the call to MediaRecorder.prepare() is throwing an error that you're ignoring, about the preview Surface already being in use, which is why start doesn't later work.

    And yes, as CommonsWare says, you likely will need a foreground service in Android P.