System designTroubleshooting

Keep Android camera preview running after MediaRecorder stops

Why Android's MediaRecorder stops the camera preview after recording — the lock, reconnect and startPreview sequence that brings the SurfaceView back.

LumaSpec Vision10 min read Share on LinkedIn

On this page 9 sections
  1. 01Why the preview dies: MediaRecorder owns the camera
  2. 02The seven-step stop order
  3. 03The causes, ranked
  4. 04Budget the recorder caps
  5. 05Front camera and continuous autofocus
  6. 06Own the surface lifecycle
  7. 07If the app is new, use CameraX
  8. 08Decision and checks before you ship
  9. 09Questions

You have an Android device that must do two jobs at once: keep the live camera picture on a SurfaceView from app start to app close, and record clips on demand through MediaRecorder. After the first recording stops, the surface goes black and stays black. This is not a driver bug; it is the ownership rule of the camera service, and the fix is a fixed order of calls in your stop path. This guide gives that order, the causes ranked by probability, a bitrate budget for the recorder caps, and the front-camera and continuous-autofocus settings.

Why the preview dies: MediaRecorder owns the camera

The Android camera service admits one client per camera ID at a time. Two designs hand that client to MediaRecorder, and the choice decides whether your preview survives a recording.

Most first versions use the implicit design: set the video source to MediaRecorder.VideoSource.CAMERA and never pass a Camera object. The recorder then opens the camera hardware itself, drives its own preview onto the surface it was given with setPreviewDisplay(), and the app's Camera instance either never opened or sits disconnected for the whole recording. When stop() runs, the recorder lets go of the camera and nothing in the code starts a new preview. That is the black screen, and it explains why the picture disappears exactly when the file closes.

The documented design shares your instance. Call camera.unlock() once before recording, hand the object over with mediaRecorder.setCamera(camera), and the same instance switches between preview and video capture without a teardown — the android.hardware.Camera class documentation walks this sequence as the standard video-recording flow. Even with the shared instance, restart the preview in the stop path — several camera HAL implementations drop preview frames around a recording — and the seven steps below cost nothing.

The one-owner rule is the same discipline industrial cameras follow: Stop and start camera streaming without crashing paired sensors covers the twin case of two applications sharing one device.

The seven-step stop order

Run this order every time a recording ends — from the stop button, from the info listener and from the surface callbacks:

  1. Guard the call. Only run it when isRecording is true; stop() outside a recording throws IllegalStateException, and an uncaught one ends the process.
  2. Close the file and calm the recorder. mRecorder.stop(), then mRecorder.reset(). The MediaRecorder class documentation describes reset() as returning the object to its just-constructed state, which is what makes the next prepare() legal; call release() only when the app is done with recording.
  3. Take the camera back. Call mCamera.lock() when you shared the instance with setCamera(). When the recorder owned the camera itself, call mCamera.reconnect() instead, wrapped for IOException — reconnect() is the documented way to reclaim the camera after MediaRecorder has finished with it.
  4. Clear the old preview path with mCamera.stopPreview().
  5. Point at the live surface with mCamera.setPreviewDisplay(mHolder). A stale handle from before the recording is the second-most-common reason a restart shows nothing.
  6. Reapply the launch parameters: preview size, focus mode, exposure compensation.
  7. Bring the picture back with mCamera.startPreview().
// Illustrative members; every method is a documented android.hardware.Camera call
private Camera mCamera = null;        // opened in surfaceCreated, non-null here
private boolean isRecording = false;  // true between start() and stop()
private int previewFrames = 0;        // test counter for the callback below

private void restorePreviewAfterStop() {
    if (mRecorder != null && isRecording) {
        mRecorder.stop();
        mRecorder.reset();
        isRecording = false;
    }
    mCamera.lock();                   // harmless if the instance was never shared
    mCamera.stopPreview();
    try {
        mCamera.setPreviewDisplay(mHolder);
        mCamera.startPreview();       // picture returns here
    } catch (IOException surfaceGone) {
        // rebuild in surfaceCreated(); do not finish() the activity
    }
}

Prove the result instead of trusting the screen. Register mCamera.setPreviewCallback() and count onPreviewFrame() deliveries into previewFrames. On a bench device the first frame lands within a few hundred milliseconds of startPreview(), and the counter runs at a typical 15–30 fps at a 640 × 480 preview size. A counter pinned at zero means the surface handle or the parameter block is at fault, not the recorder. Restarting the preview on demand, without reopening the device, is the same pattern as Restart a live camera preview without reopening the device.

The causes, ranked

Symptom Likely cause Test Fix
Black surface right after stop() Preview never restarted Log either side of startPreview() The seven-step order above
Black surface for the whole recording Recorder opened the camera itself Call Camera.open() mid-recording; it fails Share the instance with setCamera()
Crash inside stop() stop() with no recording, or too soon after start() Guard isRecording, catch IllegalStateException reset() in the catch path
Crash on the next prepare() Recorder reused after stop() without reset() Read the exception state reset() first, or release() and rebuild
stop() fails on one device model only Preview size differs from the recording size Log getPreviewSize() beside the profile size Set the preview size from the same profile
Recording ends when the screen turns off Doze and battery saver suspend background work Record with the screen off on the bench FLAG_KEEP_SCREEN_ON on the window
Preview dead after a rotation Surface destroyed and never rebuilt Rotate on the bench Rebuild in surfaceCreated()
No preview from app launch mCamera never opened; the empty catch hides it Log the object in surfaceCreated() Open it before setPreviewDisplay()

For a preview that starts and then holds a single frame, the fault sits elsewhere in the pipeline: Camera preview freezes after first frame: channel checks works that case.

Budget the recorder caps

The start point most apps copy sets setMaxDuration(60 * 1000) and setMaxFileSize(50 * 1000 * 1000) — a 60,000 ms duration cap and a 50,000,000 byte file cap. Both caps end a recording by firing an info event, and every firing runs your stop path, so the restart logic executes far more often than the stop button does.

Budget the file cap against the encoder bitrate before trusting the duration cap:

bitrate ceiling = file cap × 8 (bits/byte) ÷ duration cap 6.7 Mbit/s = 50,000,000 B × 8 ÷ 60 s

A 1080p CamcorderProfile.QUALITY_HIGH profile carries videoBitRate values of 12–17 Mbit/s on current devices; each OEM picks its own. At 17 Mbit/s the file cap stops the clip after 400,000,000 ÷ 17,000,000 ≈ 24 s; at 12 Mbit/s, after 400,000,000 ÷ 12,000,000 ≈ 33 s. The AAC audio stream adds about 1 MB per minute at 128 kbit/s (128,000 × 60 ÷ 8 = 960,000 B), pulling the stop point a few seconds earlier still. Wire a MediaRecorder.OnInfoListener and treat MEDIA_RECORDER_INFO_MAX_DURATION_REACHED and MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED exactly like a button press: reset the recorder, restart the preview.

Also set the profile: CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH) with setProfile() fixes resolution, frame rate and bitrate together, while leaving it unset lets every device choose its own encoder defaults — which is how the preview-versus-recording size mismatch in the table above appears. The CamcorderProfile class documentation lists the fields a profile fills.

Front camera and continuous autofocus

Replace a hard-coded Camera.open(1) with enumeration. getNumberOfCameras() returns the count, getCameraInfo() fills a CameraInfo, and the id whose facing equals CameraInfo.CAMERA_FACING_FRONT is the front sensor. The order of ids is a device decision, so a fixed index opens the wrong sensor on the next hardware revision.

For focus, keep the supported-mode log the first version already prints, then pick from it:

  • FOCUS_MODE_CONTINUOUS_VIDEO while recording — continuous focus intended for video capture; no autoFocus() call needed.
  • FOCUS_MODE_CONTINUOUS_PICTURE for a preview-only screen.
  • FOCUS_MODE_AUTO, driven by a periodic autoFocus() call, when only that mode is supported.
  • FOCUS_MODE_INFINITY as the last resort on fixed-focus front sensors, common on cost-reduced hardware.

Delete the takePicture(null, null, null) inside the AutoFocusCallback. It fires a still capture on every focus event, a still capture during a recording is an invalid camera state, and every still capture needs its own preview restart afterwards. Capture from a shutter button, and restart the preview in the picture callback.

Reapplying these parameters belongs in the same restore function. Pushing settings into a running stream without stalling it is a shared problem across camera software: Apply camera settings while streaming without stalling capture covers the vision-camera side.

Own the surface lifecycle

surfaceCreated() opens the camera once, sets the display and starts the preview. surfaceChanged() reapplies the size and restarts the preview. surfaceDestroyed() stops a running recording (stop() then reset()), stops the preview and releases the camera. It never calls finish(): a version that ends the activity from inside surfaceDestroyed() kills itself on the first rotation or home press, the recorder is never rebuilt, and the next start() runs on a released object and crashes.

Release in onPause() too and reopen in onResume(). Holding the camera across the background invites the system to take it away, and the next launch reports the camera as busy. The same release-and-recover discipline governs vision cameras after a fault — Release a camera after a crash on Linux: GigE restart fix walks that path. For a preview that must persist across screens and states, the loop in Android barcode scanning without a scanner app: your own preview loop is a working reference.

If the app is new, use CameraX

CameraX — the androidx.camera libraries, running on Android 5.0 (API 21) and later — binds Preview and VideoCapture as use cases on one camera and manages the lifecycle itself. There is no setCamera() hand-off, no reconnect(), and the preview keeps rendering across recordings by design; the CameraX preview documentation is the starting page for the use-case model. Migrating an app already written against the legacy Camera API is a release-sized change, not a patch: keep the seven-step order for now and schedule the move.

Decision and checks before you ship

Decision, by case:

  • Existing legacy Camera code that must keep its preview: adopt the seven-step order, wire the OnInfoListener for both caps, and prove every path with the frame counter. The surface then never goes black.
  • New app on API 21 or later: CameraX Preview plus VideoCapture, and the ownership problem disappears at the framework level.
  • Android device as an operator terminal beside an inspection cell: keep its camera for the human-facing picture and run inspection on dedicated hardware. Smart cameras take GigE and USB3 Vision interfaces and leave the Android camera HAL out of the measurement path entirely; building the vision device itself on embedded Linux points to board-level cameras with their own SDKs instead. The camera finder and the system configurator narrow both fields by resolution and interface.

Checks before you ship:

  • One owner per camera at any moment; the recorder receives your instance after the documented unlock() call, or owns the camera alone — never both.
  • stop → reset → lock or reconnect → stopPreview → setPreviewDisplay → startPreview in one idempotent function used by every stop path.
  • Preview size taken from the same CamcorderProfile the recorder encodes with.
  • Both caps set, the info listener wired, the bitrate budget computed for the profile you ship.
  • Focus mode read from getSupportedFocusModes(); no takePicture() in the autofocus callback.
  • Camera released in onPause() and surfaceDestroyed(); first onPreviewFrame() logged on the bench.

More recovery patterns of this kind sit on the systems topic page, and the applications team answers set-up questions through the contact page.

Questions

Can the preview keep running while MediaRecorder records?

Yes, if you hand the recorder your own Camera instance. Call `camera.unlock()`, pass it with `mediaRecorder.setCamera(camera)`, and leave startPreview() running; the shared object switches between preview and video capture without a teardown. Without setCamera(), the recorder opens the camera itself, and every Camera.open() call you make during the recording fails with the camera reported busy.

How do I open the front camera instead of the default one?

Enumerate instead of hard-coding: getNumberOfCameras() returns the count, getCameraInfo() fills each CameraInfo, and you open the id whose facing equals CameraInfo.CAMERA_FACING_FRONT. Front sensors are often fixed-focus, so read getSupportedFocusModes() before promising autofocus. The front preview also arrives mirrored, and setDisplayOrientation() is what lines it up with the screen orientation.

Does the same fix apply to Camera2 and CameraX?

No. `lock()`, `unlock()` and `reconnect()` belong to the legacy android.hardware.Camera API. Camera2 and CameraX replace the hand-off with a session model: one capture session carries the preview and record surfaces together, and the framework owns the transitions. On CameraX, Preview and VideoCapture run concurrently on a single camera, so the picture persists across recordings without any restart code.

Why do my recording and preview stop when the screen turns off?

With the screen off, the activity leaves the visible state and the system suspends background work under Doze and battery saver; the camera goes with it. For an attended capture station, set FLAG_KEEP_SCREEN_ON on the window so the screen stays awake while recording. For unattended capture, a foreground service is the minimum, and device power policies can still close the camera, so test screen-off behavior on the real hardware.