PC 连接手机实现摄像头预览

1 功能需求

(1)手机端打开摄像头实时预览;
(2)手机端作为服务端,PC端作为客户端连接;
(3)连接成功后PC端可以同时预览手机端的摄像头采集的图像;
(4)PC端点击拍照可以控制手机端拍摄一张照片,并将照片传给PC端。

2 功能模块

(1)安卓手机打开摄像头并实现预览和拍照功能;
(2)手机端开启监听,并在连接成功后将摄像头采集的数据传给PC;
(3)手机端读取PC发送的命令指令,执行相应的操作。

3 开发流程

3.1 开启摄像头实现预览

(1) 获取摄像头权限,并添加自动对焦属性。
  在应用程序的manifest.xml文件中添加使用手机摄像头权限。由于程序需要使用socket的通信过程,将手机作为服务端,因此需要添加网络权限。

 <uses-permission android:name="android.permission.CAMERA" />
 <uses-feature android:name="android.hardware.camera" />  
 <uses-feature android:name="android.hardware.camera.autofocus" />
 <uses-permission Android:name="android.permission.INTERNET"/>

(2)实现相机的实时预览。
   安卓系统使用SurfaceView即可完成预览功能。在布局文件中添加SurfaceView。activity_main.xml布局文件如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray_light"
android:orientation="vertical" >

<SurfaceView
    android:id="@+id/surview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:scaleType="fitCenter" />

</LinearLayout>

(3)在MainActivity 的onCreate中初始化SurfceView。初始化方法如下:

private void initSurfaceView() {
    surfaceView = (SurfaceView) findViewById(R.id.surview);
    surfaceHolder = surfaceView.getHolder();
    surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    surfaceHolder.setKeepScreenOn(true);
    surfaceHolder.addCallback(new Callback() {

        @Override
        public void surfaceDestroyed(SurfaceHolder arg0) {

        }

        @Override
        public void surfaceCreated(SurfaceHolder arg0) {
            // 开启摄像头
            startCamera(curCameraIndex);
        }

        @Override
        public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,
                int arg3) {

        }
    });
    surfaceView.setFocusable(true);
    surfaceView.setBackgroundColor(TRIM_MEMORY_BACKGROUND);

}

(4)SurfceView创建完成时调用开启摄像头的方法startCamera()。startCamera方法如下,其中代码含义已在注释程序中注明。

// 根据索引初始化摄像头
@SuppressLint("NewApi")
public void startCamera(int cameraIndex) {

    // 先停止摄像头
    stopCamera();
    // 再初始化并打开摄像头
    if (camera == null) {
        //打开手机摄像头,cameraIndex为摄像头索引,0代表后置摄像头,1代表前置摄像头。
        camera = Camera.open(cameraIndex);
        //创建摄像头操作工具类
        cameraUtil = new CameraUtil(camera, callback);
        //开启预览
        cameraUtil.initCamera(srcFrameHeight, srcFrameWidth, surfaceHolder);
        Log.e(TAG, "打开相机");
    }
}

// 停止并释放摄像头
public void stopCamera() {
    if (camera != null) {
        camera.setPreviewCallback(null);
        camera.stopPreview();
        camera.release();
        camera = null;
    }
}

//摄像头开启预览后采集到的数据回调接口
PreviewCallback callback = new PreviewCallback() {

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        Size size = camera.getParameters().getPreviewSize();
        try {
            if (times == 0) {
                YuvImage image = new YuvImage(data, ImageFormat.NV21,
                        size.width, size.height, null);
                if (image != null) {
                    // 将YuvImage对象转为字节数组
                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                    image.compressToJpeg(new Rect(0, 0, size.width,
                            size.height), 100, outputStream);
                    byte[] srcData = outputStream.toByteArray();
                    int len = srcData.length;

                    // 字节数组转为Bitmap
                    Bitmap src = BitmapFactory.decodeByteArray(srcData, 0,
                            len);
                    src = BitmapUtil.rotate(src, 90);
                    // 压缩Bitmap,并获取压缩后的字节数组,即可获取预览数据文件
                    // outdata数据即是待发送给PC端的数据
                    byte[] outdata = BitmapUtil.transImage(src,
                            srcFrameWidth / 4, srcFrameHeight / 4);
                    int datalen = outdata.length;

                    if (isOpen) {
                        // 写入头
                        sendData(new byte[] { (byte) 0xA0 });
                        // 写入数组长度
                        sendData(intToByteArray(datalen));
                        // 写入数据值
                        sendData(outdata);
                    }

                    // 回收Bitmap
                    if (!src.isRecycled()) {
                        src.recycle();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
};

(5)CameraUtil为设置摄像头的辅助类,代码如下:

public class CameraUtil {
    //摄像头类
    Camera camera;
    int cameraIndex;
    //预览视图宽和高
    int srcFrameWidth;
    int srcFrameHeight;
    SurfaceHolder surfaceHolder;
    PreviewCallback callback;

    public CameraUtil(Camera camera, PreviewCallback callback) {
        this.camera = camera;
        this.callback = callback;
       }

    //摄像头预览初始化
    public void initCamera(final int srcFrameWidth, final int srcFrameHeight, final SurfaceHolder surfaceHolder) {

        this.srcFrameHeight = srcFrameHeight;
        this.srcFrameWidth = srcFrameWidth;
        this.surfaceHolder = surfaceHolder;
        Camera.Parameters params = camera.getParameters();
        //params.setPreviewSize(srcFrameWidth / 4, srcFrameHeight / 4);
        //设置预览格式
        params.setPreviewFormat(ImageFormat.NV21);
        params.setPreviewFrameRate(30);
        //设置图像质量
        params.setJpegQuality(100);
        params.setPictureFormat(ImageFormat.JPEG);
        //设置预览方向
        params.set("orientation", "portrait");
        params.set("rotation", 90);
        //设置对焦模式为自动连续对焦
        params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);// 1连续对焦
        camera.setParameters(params);
        camera.setDisplayOrientation(90);
        // 设置显示图像的SurfaceView
        try {
            camera.setPreviewDisplay(surfaceHolder);
        } catch (IOException e) {
            e.printStackTrace();
        }
        camera.setPreviewCallback(callback);
        //启动预览
        camera.startPreview();
        camera.autoFocus(new AutoFocusCallback() {
        @Override
        public void onAutoFocus(boolean result, Camera camera) {

            // 自动对焦完成时回调
            if (result) {
                    initCamera(srcFrameWidth, srcFrameHeight, surfaceHolder);
                    camera.cancelAutoFocus();
                }
            }
        });
    }

    @SuppressLint("NewApi") 
    public void startCamera(int cameraIndex) {

        this.cameraIndex = cameraIndex;
        // 先停止摄像头
        stopCamera();
        // 再初始化并打开摄像头
        if (camera == null) {
            camera = Camera.open(cameraIndex);
            initCamera(srcFrameWidth, srcFrameHeight, surfaceHolder);       
        }
    }

    //停止预览
    public void stopCamera() {
        if (camera != null) {
            camera.setPreviewCallback(null);
            camera.stopPreview();
            camera.release();
            camera = null;
        }
    }
}

(6)BitmapUtil为图片操作辅助类

public class BitmapUtil {

    // Bitmap按照一定大小转为字节数组,以便写入socket进行发送
    public static byte[] transImage(Bitmap bitmap, int width, int height) {
        // bitmap = adjustPhotoRotation(bitmap, 90);
        try {
            int bitmapWidth = bitmap.getWidth();
            int bitmapHeight = bitmap.getHeight();
            float scaleWidth = (float) width / bitmapWidth;
            float scaleHeight = (float) height / bitmapHeight;
            Matrix matrix = new Matrix();
            matrix.postScale(scaleWidth, scaleHeight);
            // 创建压缩后的Bitmap
            Bitmap resizeBitemp = Bitmap.createBitmap(bitmap, 0, 0,
                bitmapWidth, bitmapHeight, matrix, false);

            // 压缩图片质量
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            resizeBitemp.compress(CompressFormat.JPEG, 80, outputStream);
            // 转为字节数组
            byte[] byteArray = outputStream.toByteArray();
            outputStream.close();

            // 回收资源
            if (!bitmap.isRecycled()) {
                bitmap.recycle();
            }
            if (!resizeBitemp.isRecycled()) {
                resizeBitemp.recycle();
            }
            return byteArray;

        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    //图片旋转操作
    public static Bitmap rotate(Bitmap bitmap, float degree) {
        Matrix matrix = new Matrix();
       // matrix.setScale(0.5f, 0.5f);// 缩小为原来的一半
        matrix.postRotate(degree);// 旋转45度 == matrix.setSinCos(0.5f, 0.5f);
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
        bitmap.getHeight(), matrix, true);
        return bitmap;
    }
}

3.2 连接手机与PC

手机与PC的连接实际上是使用socket通信完成的。将手机端作为服务器开启监听。PC端发起连接,通过adb命令的端口转换将PC的连接请求发送至手机,建立手机与PC的连接。主要操作流程如下:

(1) onCreate中注册广播的监听
注册广播监听主要是接收PC端调用adb发送的广播信息,用于通知手机端开启监听和停止监听。

    IntentFilter filter = new IntentFilter();
    filter.addAction("NotifyServiceStart");
    filter.addAction("NotifyServiceStop");
    registerReceiver(receiver, filter);

(2) 接收系统广播

public class ServiceBroadcastReceiver extends BroadcastReceiver {

    private static final String START_ACTION = "NotifyServiceStart";
    private static final String STOP_ACTION = "NotifyServiceStop";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (START_ACTION.equalsIgnoreCase(action)) {
            // 启动服务
            Log.e(TAG, "收到广播信息启动监听");
            new Thread() {
                public void run() {

                    if (serverSocket != null) {
                        try {
                            serverSocket.close();
                        } catch (IOException e) {

                            e.printStackTrace();
                        }
                    }
                    //服务端启动监听
                    doListen();
                };
            }.start();

        } else if (STOP_ACTION.equalsIgnoreCase(action)) {

        }
    }
}

(3)启动监听
手机端作为服务端,启动监听。注意对于网络的操作不能在主线程中。启动监听的方法doListen()如下:

    // 启动服务器端监听
    private void doListen() {
        serverSocket = null;
        try {
            serverSocket = new ServerSocket(SERVER_PORT);
            while (true) {
                Socket socket = serverSocket.accept();
                Log.e(TAG, "监听到设备连接,启动通信线程");
                //PC端连接成功,启动通信线程ThreadReadWriterIOSocket
                threadSocket = new ThreadReadWriterIOSocket(socket);
                new Thread(threadSocket).start();
            }
        } catch (IOException e) {
            Log.e(TAG, "服务端监听失败");
            e.printStackTrace();
        }
    }

(4)ThreadReadWriterIOSocket为负责通信的线程。
ThreadReadWriterIOSocket是实现了Runnable接口的一个线程,其主要任务就是接收PC端发送的命令信息,并将命令信息通过EventBus框架发送至主界面。

public class ThreadReadWriterIOSocket implements Runnable {

    private static String TAG = "ThreadReadWriterIOSocket";
    private Socket client;
    private BufferedOutputStream out;
    private BufferedInputStream in;
    boolean isConnecting = false;
    private String cmd = "";

    public ThreadReadWriterIOSocket(Socket client) {
        this.client = client;
    }

    @Override
    public void run() {

        Log.e(TAG, "有客户端连接上");
        isConnecting = true;
        try {
            // 获取输入输出流
            out = new BufferedOutputStream(client.getOutputStream());
            in  = new BufferedInputStream(client.getInputStream());

            // 循环等待,接受PC端的命令
            while (isConnecting) {
                try {
                    if (!client.isConnected()) {
                        break;
                    }
                    // 读取命令
                    cmd = readCMDFromSocket(in);    
                    Log.e(TAG, "读取到PC发送的命令" + cmd);
                    /* 根据命令分别处理数据 */
                    if (cmd.equals(Constant.CONNECT)) {// 收到连接命令
                        EventBus.getDefault().post(new MessageEvent(Constant.START));
                        out.flush();
                    } else if (cmd.equalsIgnoreCase(Constant.DISCONNECT)) {// 断开命令
                        EventBus.getDefault().post(new MessageEvent(Constant.STOP));
                        out.flush();
                    }else if (cmd.equals(Const    ant.TAKEPHOTO)) {//拍照命令
                        EventBus.getDefault().post(new MessageEvent(Constant.TAKEPHOTO));
                        out.flush();
                    }
                    in.reset();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            out.close();
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }

    public void cancel() {
        isConnecting = false;
    }

    //发送数据
    public void writeData(byte[] data) {
        if (out != null) {
            try {
                out.write((data));
            } catch (IOException e) {
                Log.e(TAG, "输入输出异常");
                e.printStackTrace();
            }
top Created with Sketch.