實(shí)時(shí)圖像傳輸?shù)脑掃€是用UDP比較好,速度比TCP快,反正丟一些幀也沒有關(guān)系
照例先上圖
電腦端

手機(jī)端

項(xiàng)目:http://pan.baidu.com/s/1pLrYrij
Android端
首先修改AndroidManifest.xml文件
添加這兩個(gè)權(quán)限
<uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name="android.permission.CAMERA"/>
然后把MainActiviy設(shè)置為橫屏
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>很簡單,只是一個(gè)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"; //服務(wù)器端的IP地址 private Camera mCamera; private Camera.Size previewSize; //預(yù)覽圖像的寬高 private DatagramSocket packetSenderSocket; //發(fā)送圖像幀的套接字 private long lastSendTime; //上一次發(fā)送圖像幀的時(shí)間 private InetAddress serverAddress; //服務(wù)端地址 private final LinkedList<DatagramPacket> packetList = new LinkedList<>(); //圖像數(shù)據(jù)包隊(duì)列 @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); //開啟通訊連接線程,連接服務(wù)端 new ConnectThread().start(); } @Override public void surfaceCreated(SurfaceHolder holder) { //獲取相機(jī) if (mCamera == null) { mCamera = Camera.open(); //打開后攝像頭 Camera.Parameters parameters = mCamera.getParameters(); //設(shè)置預(yù)覽圖大小 //注意必須為parameters.getSupportedPreviewSizes()中的長寬,否則會報(bào)異常 parameters.setPreviewSize(960, 544); previewSize = parameters.getPreviewSize(); //設(shè)置自動對焦 parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); mCamera.setParameters(parameters); mCamera.cancelAutoFocus(); //設(shè)置回調(diào) try { mCamera.setPreviewDisplay(holder); mCamera.setPreviewCallback(this); } catch (IOException e) { e.printStackTrace(); } } //開始預(yù)覽 mCamera.startPreview(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { //釋放相機(jī) if (mCamera != null) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } /** * 獲取每一幀的圖像數(shù)據(jù) */ @Override public void onPreviewFrame(byte[] data, Camera camera) { long curTime = System.currentTimeMillis(); //每20毫秒發(fā)送一幀 if (serverAddress != null && curTime - lastSendTime >= 20) { lastSendTime = curTime; //NV21格式轉(zhuǎn)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); //添加到隊(duì)尾 synchronized (packetList) { packetList.addLast(packet); } } } /** * 連接線程 */ private class ConnectThread extends Thread { @Override public void run() { super.run(); try { //創(chuàng)建連接 packetSenderSocket = new DatagramSocket(); Socket socket = new Socket(SERVER_IP, TCP_PORT); serverAddress = socket.getInetAddress(); //斷開連接 socket.close(); //啟動發(fā)送圖像數(shù)據(jù)包的線程 new ImgSendThread().start(); } catch (IOException e) { e.printStackTrace(); } } } /** * 發(fā)送圖像數(shù)據(jù)包的線程 */ private class ImgSendThread extends Thread { @Override public void run() { super.run(); while (packetSenderSocket != null) { DatagramPacket packet; synchronized (packetList) { //沒有待發(fā)送的包 if (packetList.isEmpty()) { try { Thread.sleep(10); continue; } catch (InterruptedException e) { e.printStackTrace(); } } //取出隊(duì)頭 packet = packetList.getFirst(); packetList.removeFirst(); } try { packetSenderSocket.send(packet); } catch (IOException e) { e.printStackTrace(); } } } }}surfaceCreated()、surfaceChanged()、surfaceDestroyed()這三個(gè)方法是對應(yīng)SurfaceView的,做過Android自定義相機(jī)的都知道的。需要注意的是,在surfaceCreated()中我設(shè)置了預(yù)覽圖像的寬高parameters.setPreviewSize(960, 544);這個(gè)值我根據(jù)我手機(jī)屏幕的大小設(shè)置的,因?yàn)轭A(yù)覽圖會在SurfaceView上拉伸成SurfaceView的寬高,所以設(shè)置成跟SurfaceView的寬高比差不多,這樣看上去圖像不會變形。而且它不能隨便設(shè)置,設(shè)置得不對的話會報(bào)異常,可以通過parameters.getSupportedPreviewSizes()來查看手機(jī)支持的寬高列表,不同的手機(jī)所支持的不一定相同,所以如果在這里報(bào)錯(cuò)的話可以修改成你手機(jī)所支持的值。onPreviewFrame()就是獲取每一幀圖像的地方了,在這里獲取攝像頭的圖像幀,并轉(zhuǎn)為JPEG格式。還有因?yàn)閁DP每個(gè)包的大小不能超過64K,減去一下包頭那些所占用的,大概還有65500個(gè)字節(jié)。因?yàn)槲疫@里是每一個(gè)包封裝一幀圖像,所以要注意壓縮圖像,不要超過65500個(gè)字節(jié),否則在電腦端就看不到超出的那部分圖像了。
C#
窗體很簡單,就是一個(gè)PictureBox放在Form上,把PictrureBox的Name設(shè)置為pictureBox,并讓它充滿Form。設(shè)置Form的大小為978, 591,這樣pictureBox的大小就是960, 544了,跟預(yù)覽圖的大小一致
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) { //創(chuàng)建等待連接線程 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("監(jiān)聽中"); Socket clientSocket = serverSocket.Accept(); Debug.WriteLine("連接成功"); remote = (IPEndPoint) clientSocket.RemoteEndPoint; client = new UdpClient(new IPEndPoint(IPAddress.Any, UDP_PORT)); //關(guān)閉套接字 serverSocket.Close(); clientSocket.Close(); //啟動接收線程 Thread thread = new Thread(new ThreadStart(recvImage)); thread.IsBackground = true; thread.Start(); } //接收圖像幀,并顯示到PictureBox上 private void recvImage() { while(true) { //接受圖像幀數(shù)據(jù) byte[] recvBuf = client.Receive(ref remote); MemoryStream ms = new MemoryStream(recvBuf); try { //顯示到pictureBox上 pictureBox.Image = Image.FromStream(ms); } catch (ArgumentException) { } } } }}這里就是等待手機(jī)端連接然后不斷地接收圖像數(shù)據(jù)并顯示在PictureBox上
你可能會問,不是UDP嗎,怎么這里還用了TCP?我這里用TCP是為了保證連接的建立,確認(rèn)連接成功后再通過UDP發(fā)送圖像數(shù)據(jù),否則的話服務(wù)端還沒上線手機(jī)就開始不斷地發(fā)送圖像數(shù)據(jù)了。而且如果需要的話還可以用TCP的可靠連接來發(fā)送除了圖像以外的其它數(shù)據(jù),比如文字什么的。
新聞熱點(diǎn)
疑難解答
圖片精選