功能需求:
1 手机端打开摄像头实时预览;
2 手机端作为服务端,PC端作为客户端连接;
3 连接成功后PC端可以同时预览手机端的摄像头采集的图像;
4 PC端点击拍照可以控制手机端拍摄一张照片,并将照片传给PC端。
功能模块:
1 安卓手机打开摄像头并实现预览和拍照功能;
2 手机端开启监听,并在连接成功后将摄像头采集的数据传给PC;
3 手机端读取PC发送的命令指令,执行相应的操作。
(一)开启摄像头实现预览
(1) 获取摄像头权限,并添加自动对焦属性
在应用程序的manifest.xml中添加
<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" />
(2) 实现预览
安卓系统使用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>
在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); }
当SurfceView创建时开启摄像头,开启方法如下:
// 根据索引初始化摄像头@SuppressLint("NewApi")public void startCamera(int cameraIndex) { // 先停止摄像头 stopCamera(); // 再初始化并打开摄像头 if (camera == null) { 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(); } } };
CameraUtil为设置摄像头的辅助类,代码如下:
import java.io.IOException;import android.annotation.SuppressLint;import android.graphics.ImageFormat;import android.hardware.Camera;import android.hardware.Camera.AutoFocusCallback;import android.hardware.Camera.PreviewCallback;import android.view.SurfaceHolder;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; } } }
BitmapUtil 为图片操作辅助类
import java.io.ByteArrayOutputStream;import android.graphics.Bitmap;import android.graphics.Matrix;import android.graphics.Bitmap.CompressFormat;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; } }
(二) 连接功能实现
(1) onCreate中注册广播的监听
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)) { } } }
启动监听的代码:
// 启动服务器端监听 private void doListen() { serverSocket = null; try { serverSocket = new ServerSocket(SERVER_PORT); while (true) { Socket socket = serverSocket.accept(); Log.e(TAG, "监听到设备连接,启动通信线程"); threadSocket = new ThreadReadWriterIOSocket(socket); new Thread(threadSocket).start(); } } catch (IOException e) { Log.e(TAG, "服务端监听失败"); e.printStackTrace(); } }
ThreadReadWriterIOSocket为负责通信的线程,代码如下:
import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.IOException;import java.io.InputStream;import java.net.Socket;import org.greenrobot.eventbus.EventBus;import android.util.Log;import com.myn.usb.bean.MessageEvent;import com.myn.usb.utils.Constant;/** * 数据读写线程 * * @author Administrator * */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(); } } } /* 读取命令 */ public String readCMDFromSocket(InputStream in) { int MAX_BUFFER_BYTES = 2048; String msg = ""; byte[] tempbuffer = new byte[MAX_BUFFER_BYTES]; try { int numReadedBytes = in.read(tempbuffer, 0, tempbuffer.length); msg = new String(tempbuffer, 0, numReadedBytes, "utf-8"); tempbuffer = null; } catch (Exception e) { Log.e(TAG, "readCMDFromSocket读数异常" + e.toString()); EventBus.getDefault().post(new MessageEvent(Constant.DISCONNECT)); e.printStackTrace(); } return msg; } }
(3) 传递命令信息
通信的通知过程采用EventBus实现
onCreate中注册EventBus
EventBus.getDefault().register(this);
实现事件处理函数
@Subscribe(threadMode = ThreadMode.MAIN)public void onMessageEvent(MessageEvent event) { switch (event.message) { case Constant.DISCONNECT: Toast.makeText(MainActivity.this, "客户端断开", Toast.LENGTH_LONG) .show(); threadSocket.cancel(); case Constant.START://收到连接命令 isOpen = true; Toast.makeText(MainActivity.this, "客户端连接上", Toast.LENGTH_LONG) .show(); startCamera(curCameraIndex); break; case Constant.STOP://收到断开命令 stopCamera(); isOpen = false; break; case Constant.TAKEPHOTO://收到拍照命令 if (isOpen) { camera.takePicture(new ShutterCallback() { @Override public void onShutter() { } }, new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] arg0, Camera arg1) { } }, new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { // 向电脑端发送数据 int datalen = data.length; if (isOpen) { // 写入头 sendData(new byte[] { (byte) 0xA1 }); // 写入数组长度 sendData(intToByteArray(datalen)); // 写入数据值 sendData(data); } // 重新浏览 camera.stopPreview(); camera.startPreview(); } }); } } }
当建立连接之后可以向PC端发送数据,发送数据的方法:
public void sendData(final byte[] data) { threadSocket.writeData(data); }
PC端程序代码
PC端作为客户端需要向服务端发起连接。
主函数:
public class Main { private static Socket socket; private static OutputStream out; private static InputStream in; public static MyFrame frame; private static String CMD = ""; public static void main(String[] args) { frame = new MyFrame(); JPanel panel2 = new JPanel(); panel2.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 0)); final JButton button = new JButton("连接"); panel2.add(button); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { new Thread() { public void run() { onConnect(); }; }.start(); button.setEnabled(false); } }); JButton button2 = new JButton("断开"); button2.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { CMD = Constant.DISCONNECT; try { out.flush(); out.write(CMD.getBytes()); out.flush(); } catch (IOException e) { e.printStackTrace(); } button.setEnabled(true); } }); panel2.add(button2); JButton button3 = new JButton("拍照"); button3.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { CMD = Constant.TAKEPHOTO; try { out.flush(); out.write(CMD.getBytes()); out.flush(); } catch (IOException e) { e.printStackTrace(); } } }); panel2.add(button3); frame.add(panel2, BorderLayout.SOUTH); frame.setResizable(false); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } //启动连接时使用adb发送系统广播 protected static void onConnect() { try { // adb 指令 Runtime.getRuntime().exec( "adb shell am broadcast -a NotifyServiceStop"); Thread.sleep(2000); Runtime.getRuntime().exec("adb forward tcp:12580 tcp:10086"); // 端口转换 Thread.sleep(2000); Runtime.getRuntime().exec( "adb shell am broadcast -a NotifyServiceStart"); Thread.sleep(3000); } catch (Exception e) { e.printStackTrace(); } int len = 0; int degree = 0; try { InetAddress serveraddr = null; serveraddr = InetAddress.getByName("127.0.0.1"); System.out.println("TCP 1 " + "正在连接。。。。"); socket = new Socket(serveraddr, 12580); System.out.println("TCP 2 " + "连接成功 "); // 获取输入输出流 out = new BufferedOutputStream(socket.getOutputStream()); in = new BufferedInputStream(socket.getInputStream()); // 发送连接命令 CMD = Constant.CONNECT; out.write(CMD.getBytes()); out.flush(); boolean flag = true; if (socket.isConnected()) { while (flag) { try { int head = in.read(); if (head == 0xA0) { byte[] src = new byte[4]; len = in.read(src); len = byteArrayToInt(src); if (len > 0) { int cclen = 0; byte[] srcData = new byte[len]; while (cclen < len) { int readlen = in.read(srcData, cclen, len - cclen); cclen += readlen; } ImageIcon icon = new ImageIcon(srcData); if (icon != null) { frame.setIcon(icon, degree); } } }else if (head == 0xA1) { byte[] src = new byte[4]; len = in.read(src); len = byteArrayToInt(src); if (len > 0) { int cclen = 0; byte[] srcData = new byte[len]; while (cclen < len) { int readlen = in.read(srcData, cclen, len - cclen); cclen += readlen; } //保存图片文件 File file = new File("D:\\images\\" + System.currentTimeMillis() + ".jpg"); if (!file.exists()) { file.mkdirs(); } ImageIcon icon = new ImageIcon(srcData); Image image = icon.getImage(); BufferedImage bi = new BufferedImage(image.getWidth(null),image.getHeight(null),BufferedImage.TYPE_INT_RGB); Graphics2D g2d = bi.createGraphics(); g2d.drawImage(image, 0, 0, null); g2d.dispose(); ImageIO.write(bi,"jpg",file); } } } catch (Exception e) { } } } } catch (Exception e1) { System.out.println(" 连接出现异常:连接失败: " + e1.toString()); } }// 读取数据线程,当点击连接时启动class ReadThread extends Thread { InputStream in; boolean isReading = true; public ReadThread(InputStream inputStream) { this.in = inputStream; } @Override public void run() { int len = 0; int degree = 0; while (isReading) { try { if (in.read() == 0xA0) { byte[] src = new byte[4]; len = in.read(src); len = byteArrayToInt(src); } // System.out.println("数据长度" + len); if (len > 0) { int cclen = 0; byte[] srcData = new byte[len]; while (cclen < len) { int readlen = in.read(srcData, cclen, len - cclen); cclen += readlen; } ImageIcon icon = new ImageIcon(srcData); if (icon != null) { frame.setIcon(icon, degree); } } } catch (Exception e) { isReading = false; } } } public void cancel() { isReading = false; } } public static int byteArrayToInt(byte[] b) { return b[3] & 0xFF | (b[2] & 0xFF) << 8 | (b[1] & 0xFF) << 16 | (b[0] & 0xFF) << 24; } }
MyFrame类
public class MyFrame extends JFrame { private static final long serialVersionUID = 1L; public MyPanel panel; public MyFrame() { // 默认的窗体名称 this.setTitle("USB连接显示"); // 获得面板的实例 panel = new MyPanel(); this.add(panel); this.pack(); this.setVisible(true); } //设置图片 public void setIcon(ImageIcon incon, int drgree) { panel.setImage(incon, drgree); } }
MyPanel类
public class MyPanel extends Panel { private static final long serialVersionUID = 1L; private Image bufferImage; private final Image screenImage = new BufferedImage(800, 600, 2); private final Graphics2D screenGraphic = (Graphics2D) screenImage .getGraphics(); int degree = 90; private Image backgroundImage = screenImage; public MyPanel() { // 设定焦点在本窗体 setFocusable(true); // 设定初始构造时面板大小,这里先采用图片的大小 setPreferredSize(new Dimension(800, 600)); // 绘制背景 drawView(); } public void setImage(ImageIcon icon, int degree) { //System.out.println("设置图片 偏转角度" + degree); this.degree = degree; backgroundImage = icon.getImage(); drawView(); repaint(); } private void drawView() { int width = getWidth(); int height = getHeight(); int x = width / 2 - backgroundImage.getWidth(null) /2; int y = height / 2 - backgroundImage.getHeight(null) / 2; screenGraphic.drawImage(backgroundImage, x , y, null); } @Override public void update(Graphics g) { if (bufferImage == null) { bufferImage = this.createImage(this.getWidth(), this.getHeight()); } //bufferImage = rotateImage(bufferimage, degree); Graphics gBuffer = bufferImage.getGraphics();// 获得图片缓冲区的画笔 if (gBuffer != null){ paint(gBuffer); } else{ paint(g); } gBuffer.dispose(); g.drawImage(bufferImage, 0, 0, null); } public void paint(Graphics g) { g.drawImage(screenImage, 0, 0, null); } public static BufferedImage rotateImage(final BufferedImage bufferedimage, final int degree) { int w = bufferedimage.getWidth(); int h = bufferedimage.getHeight(); int type = bufferedimage.getColorModel().getTransparency(); BufferedImage img; Graphics2D graphics2d; (graphics2d = (img = new BufferedImage(w, h, type)) .createGraphics()).setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); graphics2d.rotate(Math.toRadians(degree), w / 2, h / 2); graphics2d.drawImage(bufferedimage, 0, 0, null); graphics2d.dispose(); return img; } }
两个工程均用到的Contant类
public class Constant { public static final String addrIp = "192.168.0.119"; public static final int addrPort = 56168; public static final String CONNECT = "CONNECT"; public static final String DISCONNECT = "DISCONNECT"; public static final String TAKEPHOTO = "TAKEPHOTO"; public static final String START = "START"; public static final String STOP = "STOP"; }
上述代码只是简单实现了手机端与PC端通过USB线的数据传输,数据的通信过程与Socket一致。
源码下载
作者:进击的Hello_World
链接:https://www.jianshu.com/p/42fa9c49bb41