实时图像传输的话还是用UDP比较好,速度比TCP快,反正丢一些帧也没有关系
照例先上图
电脑端
手机端
项目:http://pan.baidu.com/s/1pLrYrij
Android端
首先修改AndroidManifest.xml文件
添加这两个权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
然后把MainActiviy设置为横屏
android:screenOrientation="landscape"
然后修改activity_main.xml文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
很简单,只是一个SurfaceView
最后就是MainActivity.java了
package com.ffpy.cameratransmitclient;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.util.LinkedList;
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback, Camera.PreviewCallback {
private final int TCP_PORT = 3333; //TCP通讯的端口号
private final int UDP_PORT = 4444; //UDP通讯的端口号
private final String SERVER_IP = "192.168.1.104"; //服务器端的IP地址
private Camera mCamera;
private Camera.Size previewSize; //预览图像的宽高
private DatagramSocket packetSenderSocket; //发送图像帧的套接字
private long lastSendTime; //上一次发送图像帧的时间
private InetAddress serverAddress; //服务端地址
private final LinkedList<DatagramPacket> packetList = new LinkedList<>(); //图像数据包队列
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final SurfaceView surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
SurfaceHolder holder = surfaceView.getHolder();
holder.setKeepScreenOn(true); //保持屏幕常亮
holder.addCallback(this);
//开启通讯连接线程,连接服务端
new ConnectThread().start();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
//获取相机
if (mCamera == null) {
mCamera = Camera.open(); //打开后摄像头
Camera.Parameters parameters = mCamera.getParameters();
//设置预览图大小
//注意必须为parameters.getSupportedPreviewSizes()中的长宽,否则会报异常
parameters.setPreviewSize(960, 544);
previewSize = parameters.getPreviewSize();
//设置自动对焦
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
mCamera.setParameters(parameters);
mCamera.cancelAutoFocus();
//设置回调
try {
mCamera.setPreviewDisplay(holder);
mCamera.setPreviewCallback(this);
} catch (IOException e) {
e.printStackTrace();
}
}
//开始预览
mCamera.startPreview();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//释放相机
if (mCamera != null) {
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
/**
* 获取每一帧的图像数据
*/
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
long curTime = System.currentTimeMillis();
//每20毫秒发送一帧
if (serverAddress != null && curTime - lastSendTime >= 20) {
lastSendTime = curTime;
//NV21格式转JPEG格式
YuvImage image = new YuvImage(data, ImageFormat.NV21, previewSize.width ,previewSize.height, null);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, previewSize.width, previewSize.height), 40, bos);
int packMaxSize = 65500; //防止超过UDP包的最大大小
byte[] imgBytes = bos.toByteArray();
Log.i("tag", imgBytes.length + "");
//打包
DatagramPacket packet = new DatagramPacket(imgBytes, imgBytes.length > packMaxSize ? packMaxSize : imgBytes.length,
serverAddress, UDP_PORT);
//添加到队尾
synchronized (packetList) {
packetList.addLast(packet);
}
}
}
/**
* 连接线程
*/
private class ConnectThread extends Thread {
@Override
public void run() {
super.run();
try {
//创建连接
packetSenderSocket = new DatagramSocket();
Socket socket = new Socket(SERVER_IP, TCP_PORT);
serverAddress = socket.getInetAddress();
//断开连接
socket.close();
//启动发送图像数据包的线程
new ImgSendThread().start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 发送图像数据包的线程
*/
private class ImgSendThread extends Thread {
@Override
public void run() {
super.run();
while (packetSenderSocket != null) {
DatagramPacket packet;
synchronized (packetList) {
//没有待发送的包
if (packetList.isEmpty()) {
try {
Thread.sleep(10);
continue;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取出队头
packet = packetList.getFirst();
packetList.removeFirst();
}
try {
packetSenderSocket.send(packet);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
surfaceCreated()、surfaceChanged()、surfaceDestroyed()这三个方法是对应SurfaceView的,做过Android自定义相机的都知道的。需要注意的是,在surfaceCreated()中我设置了预览图像的宽高
parameters.setPreviewSize(960, 544);
这个值我根据我手机屏幕的大小设置的,因为预览图会在SurfaceView上拉伸成SurfaceView的宽高,所以设置成跟SurfaceView的宽高比差不多,这样看上去图像不会变形。
而且它不能随便设置,设置得不对的话会报异常,可以通过parameters.getSupportedPreviewSizes()来查看手机支持的宽高列表,不同的手机所支持的不一定相同,所以如果在这里报错的话可以修改成你手机所支持的值。
onPreviewFrame()就是获取每一帧图像的地方了,在这里获取摄像头的图像帧,并转为JPEG格式。还有因为UDP每个包的大小不能超过64K,减去一下包头那些所占用的,大概还有65500个字节。因为我这里是每一个包封装一帧图像,所以要注意压缩图像,不要超过65500个字节,否则在电脑端就看不到超出的那部分图像了。
C#
窗体很简单,就是一个PictureBox放在Form上,把PictrureBox的Name设置为pictureBox,并让它充满Form。设置Form的大小为978, 591,这样pictureBox的大小就是960, 544了,跟预览图的大小一致
Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace CameraTransmitServer
{
public partial class Form1 : Form
{
private const int TCP_PORT = 3333; //TCP通讯的端口号
private const int UDP_PORT = 4444; //UDP通讯的端口号
private UdpClient client;
private IPEndPoint remote;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
//创建等待连接线程
Thread thread = new Thread(new ThreadStart(waitConnect));
thread.IsBackground = true;
thread.Start();
}
//等待连接
private void waitConnect()
{
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, TCP_PORT));
serverSocket.Listen(10);
Debug.WriteLine("监听中");
Socket clientSocket = serverSocket.Accept();
Debug.WriteLine("连接成功");
remote = (IPEndPoint) clientSocket.RemoteEndPoint;
client = new UdpClient(new IPEndPoint(IPAddress.Any, UDP_PORT));
//关闭套接字
serverSocket.Close();
clientSocket.Close();
//启动接收线程
Thread thread = new Thread(new ThreadStart(recvImage));
thread.IsBackground = true;
thread.Start();
}
//接收图像帧,并显示到PictureBox上
private void recvImage()
{
while(true)
{
//接受图像帧数据
byte[] recvBuf = client.Receive(ref remote);
MemoryStream ms = new MemoryStream(recvBuf);
try
{
//显示到pictureBox上
pictureBox.Image = Image.FromStream(ms);
}
catch (ArgumentException)
{ }
}
}
}
}
这里就是等待手机端连接然后不断地接收图像数据并显示在PictureBox上
你可能会问,不是UDP吗,怎么这里还用了TCP?
我这里用TCP是为了保证连接的建立,确认连接成功后再通过UDP发送图像数据,否则的话服务端还没上线手机就开始不断地发送图像数据了。而且如果需要的话还可以用TCP的可靠连接来发送除了图像以外的其它数据,比如文字什么的。