基于SurfaceTexture的静默/无预览拍照方案,公司业务需要做一个静默拍照的功能,了解了一下常见的解决方案,基本上都是基于SurfaceView做的,弊病颇多。研究了一下,决定以SurfaceTexture为切入点,做一个真正的静默拍照功能。废话不多说,上代码:
.
import android.app.Service;
import android.content.Intent;
import android.hardware.Camera;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import com.djt.launcher.bean.CaptureMsg;
import com.djt.launcher.parentalcontrol.screenshot.SlientCamera;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.lang.ref.WeakReference;
/**
*
* 一键抓拍
*/
public class SlientTackPicService extends Service {
private static final String TAG = "slient_tack_pic";
private static final int SLIENT_TACK_CAMERA = 1;
private SlientCamera slientCamera;
private int cameraStatus;
private int replySN;
private String replySEID;
public MyHandler handler = new MyHandler(this);
private static class MyHandler extends Handler {
WeakReference<SlientTackPicService> mContext;
MyHandler(SlientTackPicService mContext) {
this.mContext = new WeakReference<SlientTackPicService>(mContext);
}
@Override
public void handleMessage(Message msg) {
SlientTackPicService service = mContext.get();
if (null == service) {
Log.e(TAG, "mContext.get() is null!");
return;
}
switch (msg.what) {
case SLIENT_TACK_CAMERA:
Log.i(TAG, "camera ready, tackPicture!");
service.slientCamera.tackPicture();
break;
default:
break;
}
}
}
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "SlientTackPicService, onCreate()");
EventBus.getDefault().register(this);
slientCamera = new SlientCamera(this);
slientCamera.onCreate();
// 1是前置摄像头 0 是后置摄像头
cameraStatus = slientCamera.openCamere() ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
Log.i(TAG, "cameraStatus = " + cameraStatus);
// if (cameraStatus == 1) {
// handler.sendEmptyMessageDelayed(SLIENT_TACK_CAMERA, 300);
// }
}
// EventBus.getDefault().post(CaptureMsg()) 来启动
@Subscribe(threadMode = ThreadMode.MAIN)
public void OnMessage(CaptureMsg msg) {
slientCamera = new SlientCamera(this);
slientCamera.onCreate();
// 1是前置摄像头 0 是后置摄像头
cameraStatus = slientCamera.openCamere() ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
// if (cameraStatus == 1) {
handler.sendEmptyMessageDelayed(SLIENT_TACK_CAMERA, 300);
// }
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "onStartCommand");
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void releaseSource() {
Log.i(TAG, "releaseSource");
if (handler.hasMessages(SLIENT_TACK_CAMERA)) {
handler.removeMessages(SLIENT_TACK_CAMERA);
}
stopSelf();
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "SlientTackPicService, onDestroy()");
releaseSource();
slientCamera.onDestroy();
}
}
SlientTackPicService这个服务在后台运行,主要的拍照操作在 SlientCamera 类中执行。在SlientTackPicService的onCreate()方法中执行对SlientCamera 的初始化,并判断能否打开相机:cameraStatus 为1时表示后台打开相机成功,-1表示打开相机失败。失败原因可能是相机当前正在使用,或者其他未知原因等等。相机打开成功后,延时进行拍照操作。
下面上SlientCamera 的代码:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.os.Build;
import android.util.Log;
import com.djt.launcher.parentalcontrol.CDNFileUpload;
import com.djt.launcher.parentalcontrol.ControlBean;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
*
* Created by jxl on 2017/9/1.
*/
public class SlientCamera implements SurfaceTexture.OnFrameAvailableListener{
private static final String TAG = "slient_camera";
private Context context;
private Camera mCamera;
private SurfaceTexture surfaceTexture;
/**
* 定义图片保存的路径和图片的名字
*/
public final static String PHOTO_PATH = "mnt/sdcard/DCIM/Camera/";
public SlientCamera(Context context) {
this.context = context;
}
public void onCreate() {
Log.i(TAG, "SlientCamera, onCreate()");
surfaceTexture = new SurfaceTexture(10);
surfaceTexture.setOnFrameAvailableListener(this);
//openCamere();
}
public int getSdkVersion() {
return android.os.Build.VERSION.SDK_INT;
}
private boolean checkCameraFacing(final int facing) {
if (getSdkVersion() < Build.VERSION_CODES.GINGERBREAD) {
return false;
}
final int cameraCount = Camera.getNumberOfCameras();
Camera.CameraInfo info = new Camera.CameraInfo();
for (int i = 0; i < cameraCount; i++) {
Camera.getCameraInfo(i, info);
if (facing == info.facing) {
return true;
}
}
return false;
}
/**
* 后置摄像头
* @return
*/
public boolean hasBackFacingCamera() {
final int CAMERA_FACING_BACK = 0;
return checkCameraFacing(CAMERA_FACING_BACK);
}
/**
* 前置摄像头
* @return
*/
public boolean hasFrontFacingCamera() {
final int CAMERA_FACING_BACK = 1;
return checkCameraFacing(CAMERA_FACING_BACK);
}
public boolean openCamere() {
try {
// 1是前置摄像头 0 是后置摄像头
if (hasFrontFacingCamera()){
mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
}else {
if (hasBackFacingCamera()){
//mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
ControlBean bean = new ControlBean();
bean.setPushStatus(-1);
bean.setControlType(6);
bean.setUrl("");
bean.setErrorMsg("没有前置摄像头,一键抓拍失败");
EventBus.getDefault().post(bean);
return false;
}else {
ControlBean bean = new ControlBean();
bean.setPushStatus(-1);
bean.setControlType(6);
bean.setUrl("");
bean.setErrorMsg("没有摄像头,一键抓拍失败");
EventBus.getDefault().post(bean);
return false;
}
}
} catch (Exception e) {
e.printStackTrace();
//closeCamera();
return false;
}
mCamera.setDisplayOrientation(180);
/*List<camera.size> preList = mCamera.getParameters().getSupportedPreviewSizes();
List<camera.size> picList = mCamera.getParameters().getSupportedPictureSizes();
for (Camera.Size size:preList) {
Log.i(TAG, "PreviewSize, size.width = " + size.width + ", size.height = " + size.height);
}
for (Camera.Size size:picList) {
Log.i(TAG, "PictureSize, size.width = " + size.width + ", size.height = " + size.height);
}*/
Camera.Parameters params = mCamera.getParameters();
params.setPreviewFormat(ImageFormat.NV21);
//params.setRotation();
/*boolean flag = params.isZoomSupported();
int maxZoom = params.getMaxZoom();*/
params.setZoom(0);//设置焦距为0
// params.setPreviewSize(Constants.VIDEO_WIDTH, Constants.VIDEO_HEIGHT);
// params.setPictureSize(Constants.PICTURE_WIDTH, Constants.PICTURE_HEIGHT);
mCamera.setParameters(params);
if (mCamera == null) {
// Seeing this on Nexus 7 2012 -- I guess it wants a rear-facing camera, but
// there isn't one. TODO: fix
//throw new RuntimeException("Default camera not available");
Log.e(TAG, "openCamere, mCamera == null!");
return false;
}
try {
//这一步是最关键的,使用surfaceTexture来承载相机的预览,而不需要设置一个可见的view
mCamera.setPreviewTexture(surfaceTexture);
mCamera.startPreview();
} catch (IOException ioe) {
ioe.printStackTrace();
return false;
}
return true;
}
public void tackPicture() {
//Log.w(TAG, "tackPicture()");
// File picFile = CameraUtil.getOutputMediaFile();
// if (picFile == null) {
// Log.e(TAG, "tackPicture, getOutputMediaFile is null!");
// return;
// }
if (null == mCamera) {
Log.e(TAG, "tackPicture(), null == mCamera");
ControlBean bean = new ControlBean();
bean.setPushStatus(-1);
bean.setControlType(6);
bean.setUrl("");
EventBus.getDefault().post(bean);
return;
}
mCamera.takePicture(mShutterCallback, null, mPictureCallback);
//saveData = true;
//Log.i(TAG, "takePicture after time = " + System.currentTimeMillis());
}
private Camera.ShutterCallback mShutterCallback = new Camera.ShutterCallback() {
@Override
public void onShutter() {
//playContinuousSound();
}
};
/**
* 上传图片
* @param file
*/
private void uploadImage(File file){
CDNFileUpload.INSTANCE.uploadAnswerImageAsync(file)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(String s) {
ControlBean bean = new ControlBean();
bean.setPushStatus(1);
bean.setControlType(6);
bean.setUrl(s);
EventBus.getDefault().post(bean);
}
@Override
public void onError(Throwable e) {
ControlBean bean = new ControlBean();
bean.setPushStatus(-1);
bean.setControlType(6);
bean.setUrl("");
bean.setErrorMsg("一键抓拍失败");
EventBus.getDefault().post(bean);
}
@Override
public void onComplete() {
try {
file.delete();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public static String getPhotoFileName() {
Date date = new Date(System.currentTimeMillis());
SimpleDateFormat dateFormat = new SimpleDateFormat("'IMG'_yyyyMMdd_HHmmss");
return dateFormat.format(date) + ".jpg";
}
private void savePicture(byte[] data){
// 将得到的照片进行270°旋转,使其竖直
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
Matrix matrix = new Matrix();
matrix.preRotate(0);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
// 创建并保存图片文件
File mFile = new File(PHOTO_PATH);
if (!mFile.exists()) {
mFile.mkdirs();
}
File pictureFile = new File(PHOTO_PATH, getPhotoFileName());
FileOutputStream fos = null;
try {
fos = new FileOutputStream(pictureFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
bitmap.recycle();
fos.close();
Log.i("TAG", "拍摄成功!");
uploadImage(pictureFile);
} catch (Exception error) {
Log.e("TAG", "拍摄失败");
error.printStackTrace();
} finally {
closeCamera();
}
}
private Camera.PictureCallback mPictureCallback = new Camera.PictureCallback() {
@Override
public void onPictureTaken(final byte[] data, Camera camera) {
Log.i(TAG, "data time = " + System.currentTimeMillis());
//final File file = new File(PHOTO_PATH, getPhotoFileName());
//FileOutputStream output = null;
try {
//output = new FileOutputStream(file);
//output.write(data);
savePicture(data);
// uploadImage(file);
}catch (Exception e) {
e.printStackTrace();
ControlBean bean = new ControlBean();
bean.setPushStatus(-1);
bean.setControlType(6);
bean.setUrl("");
bean.setErrorMsg("一键抓拍失败");
EventBus.getDefault().post(bean);
}
//bitmap就是拍出来的照片,可以进行需要做的操作
//Bitmap bitmap = BitmapUtil.rotaingImageView(0, BitmapFactory.decodeByteArray(data, 0, data.length));
//Log.i(TAG, "save time = " + System.currentTimeMillis());
}
};
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
Log.i(TAG, "onFrameAvailable, surfaceTexture.getTimestamp() = " + surfaceTexture.getTimestamp());
//surfaceTexture.updateTexImage();
}
private void closeCamera() {
Log.i(TAG, "closeCamera");
if (null != mCamera) {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
public void onDestroy() {
closeCamera();
}
}
主要代码都在上面了,大概的解释一下:在SlientCamera 的初始化的时候,创建一个SurfaceTexture,这个SurfaceTexture承载了相机的预览。但是由于我们没有设置可见的TextureView,所以不会有预览的界面。关于openCamere这一段,其实是有很多坑的,比如相机前后摄像头、预览角度、预览界面之类的设置。我们公司做的业务是智能硬件,所以硬件参数是固定的,我就把这些参数写死了,实际的应用中是很麻烦的,这里不再赘述了。拍照时调用tackPicture方法,mPictureCallback 方法内的那个byte数组就是最终的照片,将其转化为bitmap即可。具体的实现很简单,就不再贴出来了。
总结一下这个思路,好处在于不需要设置一个可见的view去承载相机的预览,把这个事情交给了surfaceTexture去做,可以全程在后台静默完成,想想还是挺猥琐的。限制在于surfaceTexture只能在API11之上才能调用,而在更高等级的API21中,调用相机需要弹出权限提示框,无法再静默打开摄像头。说白了,这是一个只能在API11~API21中间使用的思路。