1

I am trying to develop an application in Android with the following feature: record video and audio seamlessly, even if the user has another application in the foreground. A common scenario for it would be: the user opens the app, starts recording, then opens a navigation app or receives a call. I want my app to keep recording.

I have put together some code, mainly inspired by this tutorial, which will be quoted below. I have encountered two problems, however:

1. When I press the "home" key, video recording freezes, but sound is fine

2. When I navigate back to the app, the preview is black

My questions are:

  • Is my goal possible on Android?
  • What am I doing wrong?

My code:

public class GlobalState extends Application {
 private boolean recording = false;
 private boolean loggingEnabled = true;

 private Camera serviceCamera = null;
 private CameraPreview cameraPreview = null;

 @Override
 public void onCreate() {
  try {
   serviceCamera = Camera.open();
  } catch (Exception e) {

  }

  super.onCreate();
 }

 public boolean isRecording() {
  return recording;
 }

 public boolean isLoggingEnabled() {
  return loggingEnabled;
 }

 public void setRecording(boolean recording) {
  this.recording = recording;
 }

 public void setCamera(Camera serviceCamera) {
  this.serviceCamera = serviceCamera;
 }

 public Camera getCamera() {
  return serviceCamera;
 }

 public void setCameraPreview(CameraPreview cameraPreview) {
  this.cameraPreview = cameraPreview;
 }

 public CameraPreview getCameraPreview() {
  return this.cameraPreview;
 }

}

public class CameraPreview extends SurfaceView implements
  SurfaceHolder.Callback {
 private SurfaceHolder mHolder;
 private Camera mCamera;

 private static final String TAG = "CameraPreview";

 public CameraPreview(Context context, Camera camera) {
  super(context);
  mCamera = camera;

  mHolder = getHolder();
  mHolder.addCallback(this);
  mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
 }

 public void surfaceCreated(SurfaceHolder holder) {
  try {
   mCamera.setPreviewDisplay(holder);
   mCamera.startPreview();
  } catch (IOException e) {
   Log.d(TAG, "Error setting camera preview: " + e.getMessage());
  }
 }

 public void surfaceDestroyed(SurfaceHolder holder) {
 }

 public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {

  if (mHolder.getSurface() == null) {
   return;
  }

  try {
   mCamera.stopPreview();
  } catch (Exception e) {
  }

  try {
   mCamera.setPreviewDisplay(mHolder);
   mCamera.startPreview();
  } catch (Exception e) {
   Log.d(TAG, "Error starting camera preview: " + e.getMessage());
  }
 }
}

public class MainActivity extends Activity {
 public String TAG = "DE-MainActivity";

 ImageView mRecordView;
 ImageView mMenuButtonView;
 LinearLayout mMenuView;
 TextView mVideosTextView;
 TextView mSettingsTextView;

 private Camera mCamera;
 private CameraPreview mPreview;

 GlobalState mAppState = null;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  mAppState = (GlobalState) getApplicationContext();

  if (mAppState.isLoggingEnabled()) {
   Log.v(TAG, "Activity: onCreate");
  }

  mCamera = mAppState.getCamera();
  if (mAppState.getCameraPreview() == null) {
   mPreview = new CameraPreview(this, mCamera);
   mAppState.setCameraPreview(mPreview);
  }
  FrameLayout preview = (FrameLayout) findViewById(R.id.fl_camera);
  preview.addView(mPreview);

  mMenuView = (LinearLayout) findViewById(R.id.ll_menu_list);
  mVideosTextView = (TextView) findViewById(R.id.tv_menu_item_videos);
  mSettingsTextView = (TextView) findViewById(R.id.tv_menu_item_settings);

  mRecordView = (ImageView) findViewById(R.id.iv_record);
  mRecordView.setImageResource(R.drawable.btn_not_recording);
  mRecordView.setAlpha((float) 0.5);
  mRecordView.bringToFront();
  mRecordView.setOnClickListener(new View.OnClickListener() {

   @Override
   public void onClick(View v) {

    if (!mAppState.isRecording()) {

     mRecordView.setImageResource(R.drawable.btn_recording);
     mRecordView.setAlpha((float) 0.3);

     startService(new Intent(MainActivity.this,
       RecorderService.class));

    } else {

     mRecordView.setImageResource(R.drawable.btn_not_recording);
     mRecordView.setAlpha((float) 0.5);

     stopService(new Intent(MainActivity.this,
       RecorderService.class));

    }
   }
  });

  mMenuButtonView = (ImageView) findViewById(R.id.iv_menu);
  mMenuButtonView.setImageResource(R.drawable.btn_menu);
  mMenuButtonView.setAlpha((float) 0.5);
  mMenuButtonView.bringToFront();
  mMenuButtonView.setOnClickListener(new View.OnClickListener() {

   @Override
   public void onClick(View v) {
    if (mMenuView.getVisibility() == View.VISIBLE) {
     mMenuView.setVisibility(View.INVISIBLE);
    } else {
     mMenuView.setVisibility(View.VISIBLE);
    }
   }
  });

  mSettingsTextView.setOnClickListener(new View.OnClickListener() {

   @Override
   public void onClick(View v) {
    if (mAppState.isLoggingEnabled())
     Log.v(TAG, "settings clicked!");
   }
  });

  mVideosTextView.setOnClickListener(new View.OnClickListener() {

   @Override
   public void onClick(View v) {
    if (mAppState.isLoggingEnabled())
     Log.v(TAG, "videos clicked!");
   }
  });
 }

 @Override
 protected void onDestroy() {
  if (mAppState.isLoggingEnabled())
   Log.v(TAG, "APPLICATION EXIT!");

  if (mCamera != null) {
   mCamera.release(); // release the camera for other applications
   mCamera = null;
  }

  super.onDestroy();
 }

 public boolean onCreateOptionsMenu(Menu menu) {

  if (mMenuView.getVisibility() == View.VISIBLE) {
   mMenuView.setVisibility(View.INVISIBLE);
  } else {
   mMenuView.setVisibility(View.VISIBLE);
  }
  return false;
 }
}

public class RecorderService extends Service {

 private static final String TAG = "RecorderService";

 private static Camera mServiceCamera;
 private MediaRecorder mMediaRecorder;

 private GlobalState mAppState;

 public static final int MEDIA_TYPE_IMAGE = 1;
 public static final int MEDIA_TYPE_VIDEO = 2;

 @Override
 public void onCreate() {
  mAppState = (GlobalState) getApplicationContext();
  mServiceCamera = mAppState.getCamera();

  if (mAppState.isLoggingEnabled())
   Log.v(TAG, "onCreate");

 }

 @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
  super.onStartCommand(intent, flags, startId);

  if (!mAppState.isRecording()) {
   if (prepareVideoRecorder()) {
    mMediaRecorder.start();
    mAppState.setRecording(true);
   } else {
    releaseMediaRecorder();
   }
  }

  return 5;
 }

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

 @Override
 public void onDestroy() {
  if (mAppState.isLoggingEnabled())
   Log.v(TAG, "onDestroy");

  // stop recording and release camera
  mMediaRecorder.stop(); // stop the recording
  releaseMediaRecorder(); // release the MediaRecorder object
  mServiceCamera.lock(); // take camera access back from MediaRecorder

  mAppState.setRecording(false);

  super.onDestroy();
 }

 private void releaseMediaRecorder() {
  if (mMediaRecorder != null) {
   mMediaRecorder.reset(); // clear recorder configuration
   mMediaRecorder.release(); // release the recorder object
   mMediaRecorder = null;
  }
 }

 /** Create a file Uri for saving an image or video */
 private static Uri getOutputMediaFileUri(int type) {
  return Uri.fromFile(getOutputMediaFile(type));
 }

 /** Create a File for saving an image or video */
 private static File getOutputMediaFile(int type) {
  File mediaStorageDir = new File(
    Environment.getExternalStorageDirectory(), "DashEyeApp");

  if (!mediaStorageDir.exists()) {
   if (!mediaStorageDir.mkdirs()) {
    Log.d("MyCameraApp", "failed to create directory");
    return null;
   }
  }

  String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss")
    .format(new Date());
  File mediaFile;
  if (type == MEDIA_TYPE_IMAGE) {
   mediaFile = new File(mediaStorageDir.getPath() + File.separator
     + "IMG_" + timeStamp + ".jpg");
  } else if (type == MEDIA_TYPE_VIDEO) {
   mediaFile = new File(mediaStorageDir.getPath() + File.separator
     + "VID_" + timeStamp + ".mp4");
  } else {
   return null;
  }

  return mediaFile;
 }

 private boolean prepareVideoRecorder() {

  mMediaRecorder = new MediaRecorder();

  mServiceCamera.unlock();
  mMediaRecorder.setCamera(mServiceCamera);

  mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
  mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);

  mMediaRecorder.setProfile(CamcorderProfile
    .get(CamcorderProfile.QUALITY_HIGH));

  mMediaRecorder.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO)
    .toString());

  // Step 5: Set the preview output
  // mMediaRecorder.setPreviewDisplay(mAppState.getCameraPreview().getHolder().getSurface());

  try {
   mMediaRecorder.prepare();
  } catch (IllegalStateException e) {
   Log.d(TAG,
     "IllegalStateException preparing MediaRecorder: "
       + e.getMessage());
   releaseMediaRecorder();
   return false;
  } catch (IOException e) {
   Log.d(TAG, "IOException preparing MediaRecorder: " + e.getMessage());
   releaseMediaRecorder();
   return false;
  }
  return true;
 }
}
I am sorry for the wall of text and I greatly appreciate any help!

3 Answers3

1

my solution is fine, try it:

Service android:

import java.util.Calendar;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.view.Gravity;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;

public class BackgroundVideoRecorder extends Service implements
    SurfaceHolder.Callback {

private WindowManager windowManager;
private SurfaceView surfaceView;
private Camera camera = null;
private MediaRecorder mediaRecorder = null;



int contTime = 0, duracaoGravacao = 30; //interval  in seconds to record video

private class thread implements Runnable {
    public void run() {

        contTime++;

        if (contTime >= duracaoGravacao) {
            StopService();
        }

        tick_Handler.postDelayed(tick_thread, 1000);
    }
}

Handler tick_Handler;
thread tick_thread;

Preferences pref;

@Override
public void onCreate() {

    windowManager = (WindowManager) this
            .getSystemService(Context.WINDOW_SERVICE);
    surfaceView = new SurfaceView(this);
    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);

    tick_Handler = new Handler();
    tick_thread = new thread();

    VIDEO_RECORDER_FOLDER = new _Path().getPathVideo();

}

@Override
public void onStart(Intent intent, int startId) {

    tick_Handler.post(tick_thread);

}

// Method called right after Surface created (initializing and starting
// MediaRecorder)
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {

    boolean found = false;

    int i = 0;

    try {
        for (i = 0; i < Camera.getNumberOfCameras(); i++) {

            Camera.CameraInfo newInfo = new Camera.CameraInfo();

            Camera.getCameraInfo(i, newInfo);

            if (newInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
                found = true;
                break;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    if (found) {
        camera = Camera.open(i);
    } else {
        camera = Camera.open();
    }

    Calendar lCDateTime = Calendar.getInstance();

    String t = String.valueOf(lCDateTime.getTimeInMillis());

    nomeArquivo = "hire_me_now_" + t + ".mp4";

    nomeArquivo = nomeArquivo.replace(" ", "_").replace(":", "_")
            .replace("-", "_");

    String caminhoArquivo = VIDEO_RECORDER_FOLDER + "/" + nomeArquivo;

    mediaRecorder = new MediaRecorder();
    camera.unlock();

    mediaRecorder.setPreviewDisplay(surfaceHolder.getSurface());
    mediaRecorder.setCamera(camera);
    mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
    mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
    mediaRecorder.setProfile(CamcorderProfile
            .get(CamcorderProfile.QUALITY_QVGA));
    mediaRecorder.setVideoFrameRate(15);

    mediaRecorder.setOutputFile(caminhoArquivo);

    try {
        mediaRecorder.prepare();

    } catch (Exception e) {
        e.printStackTrace();
    }

    mediaRecorder.start();
}

// Stop recording and remove SurfaceView
@Override
public void onDestroy() {

    mediaRecorder.stop();
    mediaRecorder.reset();
    mediaRecorder.release();

    camera.lock();
    camera.release();

    windowManager.removeView(surfaceView);

}

protected void StopService() {
    try {
        this.stopSelf();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@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;
}

}

Elton da Costa
  • 1,279
  • 16
  • 27
  • Doesn't work at all. Caused by: android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@e08059d -- permission denied for this window type – JustRandom Jul 18 '19 at 15:56
0

It turns out that Android doesn't like that the preview gets destroyed (when, for example, the user hits the "Home" button) so it cuts out video recording.

The workaround to this is using WindowManager to set an overlay and, when the user hits the "Home" button, resize it to 1x1. I found the solution here. Many thanks to cman!

Community
  • 1
  • 1
  • The solution over the link you gave does not have a code for preview resizing. I will be greatful if you give some more hints, – Dmitri Novikov Nov 30 '18 at 13:13
  • I'be got it. We can use this code to set any size any time: `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.updateViewLayout(surfaceView, layoutParams);` – Dmitri Novikov Nov 30 '18 at 14:16
-1

it can be done but from API level 23 you will need ask for camera permissions, you can refer to this answer https://stackoverflow.com/a/49919386/4604234