后台服务无预览拍照

基于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中间使用的思路。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345