上次說到的那個Demo,趁著今天有空整理一下。
原理很簡單,雖然沒有寫過android應用,但是,嘛~ 高級語言都是相通的,自傲一下。所以簡單研究了一下api後,發現相機對象有預覽回調方法,
實現一下Camera.PreviewCallback接口,就可以得到一個每一幀畫面的回調事件,那麼思路就很簡單了。
拿到畫面後,進行下簡單的壓縮,然後把圖像用Socket傳輸到服務器上,服務器上綁定到一個窗口的picBox上就可以了。
當然,這裡還牽扯到多線程的問題,因為一個SocketServer可以實現和多個client建立連接,而每一個連接都需要獨立的線程來實現監聽。
安卓端代碼:
package com.xwg.monitorclient; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; import java.util.List; import java.util.zip.DeflaterOutputStream; import android.graphics.Rect; import android.graphics.YuvImage; import android.hardware.Camera; import android.hardware.Camera.Size; import android.os.Bundle; import android.preference.PreferenceManager; import android.app.Activity; import android.content.SharedPreferences; import android.view.Menu; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; public class MainActivity extends Activity implements SurfaceHolder.Callback, Camera.PreviewCallback{ private SurfaceView mSurfaceview = null; // SurfaceView對象:(視圖組件)視頻顯示 private SurfaceHolder mSurfaceHolder = null; // SurfaceHolder對象:(抽象接口)SurfaceView支持類 private Camera mCamera = null; // Camera對象,相機預覽 /**服務器地址*/ private String pUsername="XZY"; /**服務器地址*/ private String serverUrl="192.168.0.3"; /**服務器端口*/ private int serverPort=9999; /**視頻刷新間隔*/ private int VideoPreRate=1; /**當前視頻序號*/ private int tempPreRate=0; /**視頻質量*/ private int VideoQuality=85; /**發送視頻寬度比例*/ private float VideoWidthRatio=1; /**發送視頻高度比例*/ private float VideoHeightRatio=1; /**發送視頻寬度*/ private int VideoWidth=320; /**發送視頻高度*/ private int VideoHeight=240; /**視頻格式索引*/ private int VideoFormatIndex=0; /**是否發送視頻*/ private boolean startSendVideo=false; /**是否連接主機*/ private boolean connectedServer=false; private Button myBtn01, myBtn02; private EditText txtIP; @Override public void onStart()//重新啟動的時候 { mSurfaceHolder = mSurfaceview.getHolder(); // 綁定SurfaceView,取得SurfaceHolder對象 mSurfaceHolder.addCallback(this); // SurfaceHolder加入回調接口 mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);// 設置顯示器類型,setType必須設置 //讀取配置文件 SharedPreferences preParas = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); pUsername=preParas.getString("Username", "XZY"); serverUrl=preParas.getString("ServerUrl", "192.168.0.3"); String tempStr=preParas.getString("ServerPort", "9999"); serverPort=Integer.parseInt(tempStr); tempStr=preParas.getString("VideoPreRate", "1"); VideoPreRate=Integer.parseInt(tempStr); tempStr=preParas.getString("VideoQuality", "85"); VideoQuality=Integer.parseInt(tempStr); tempStr=preParas.getString("VideoWidthRatio", "100"); VideoWidthRatio=Integer.parseInt(tempStr); tempStr=preParas.getString("VideoHeightRatio", "100"); VideoHeightRatio=Integer.parseInt(tempStr); VideoWidthRatio=VideoWidthRatio/100f; VideoHeightRatio=VideoHeightRatio/100f; super.onStart(); } @Override protected void onResume() { super.onResume(); InitCamera(); } /**初始化攝像頭*/ private void InitCamera(){ try{ mCamera = Camera.open(); Listlist = mCamera.getParameters().getSupportedPreviewSizes(); for(Size s : list) { if(s.width<=640) { Camera.Parameters params = mCamera.getParameters(); params.setPreviewSize(s.width, s.height); mCamera.setParameters(params); break; } } } catch (Exception e) { e.printStackTrace(); } } @Override protected void onPause() { try{ if (mCamera != null) { mCamera.setPreviewCallback(null); // !!這個必須在前,不然退出出錯 mCamera.stopPreview(); mCamera.release(); mCamera = null; } } catch (Exception e) { e.printStackTrace(); } super.onPause(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //禁止屏幕休眠 getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); mSurfaceview = (SurfaceView) findViewById(R.id.camera_preview); myBtn01=(Button)findViewById(R.id.button1); myBtn02=(Button)findViewById(R.id.button2); txtIP = (EditText)findViewById(R.id.editText1); //開始連接主機按鈕 myBtn01.setOnClickListener(new OnClickListener(){ public void onClick(View v) { serverUrl = txtIP.getText().toString(); if(connectedServer){//停止連接主機,同時斷開傳輸 startSendVideo=false; connectedServer=false; myBtn02.setEnabled(false); myBtn01.setText("開始連接"); myBtn02.setText("開始傳輸"); //斷開連接 //Thread th = new MySendCommondThread("PHONEDISCONNECT|"+pUsername+"|"); //th.start(); } else//連接主機 { //啟用線程發送命令PHONECONNECT connectedServer=true; myBtn02.setEnabled(true); myBtn01.setText("停止連接"); } }}); myBtn02.setEnabled(false); myBtn02.setOnClickListener(new OnClickListener(){ public void onClick(View v) { if(startSendVideo)//停止傳輸視頻 { startSendVideo=false; myBtn02.setText("開始傳輸"); } else{ // 開始傳輸視頻 Thread th = new MyThread(); th.start(); startSendVideo=true; myBtn02.setText("停止傳輸"); } }}); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { // TODO Auto-generated method stub if (mCamera == null) { return; } mCamera.stopPreview(); mCamera.setPreviewCallback(this); mCamera.setDisplayOrientation(90); //設置橫行錄制 //獲取攝像頭參數 Camera.Parameters parameters = mCamera.getParameters(); Size size = parameters.getPreviewSize(); VideoWidth=size.width; VideoHeight=size.height; VideoFormatIndex=parameters.getPreviewFormat(); mCamera.startPreview(); } @Override public void surfaceCreated(SurfaceHolder holder) { // TODO Auto-generated method stub try { if (mCamera != null) { mCamera.setPreviewDisplay(mSurfaceHolder); mCamera.startPreview(); } } catch (IOException e) { e.printStackTrace(); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { // TODO Auto-generated method stub if (null != mCamera) { mCamera.setPreviewCallback(null); // !!這個必須在前,不然退出出錯 mCamera.stopPreview(); mCamera = null; } } @Override public void onPreviewFrame(byte[] data, Camera camera) { // TODO Auto-generated method stub //如果沒有指令傳輸視頻,就先不傳 if(!startSendVideo) return; // if(tempPreRate >8)&0xff); rtn[3] = (byte)((len>>16)&0xff); rtn[4] = (byte)(len>>>24); return rtn; } /**發送命令線程*/ class MyThread extends Thread{ public void run(){ //實例化Socket try { client=new Socket(serverUrl,serverPort); } catch (UnknownHostException e) { } catch (IOException e) { } } } /**發送文件線程*/ class MySendFileThread extends Thread{ byte[] content = null; public MySendFileThread(byte[] content){ this.content = content; } public void run() { try{ SendMessage(this.content); //tempSocket.close(); } catch (Exception e) { e.printStackTrace(); } } } }
安卓端代碼通過一個全局Socket創建連接,然後通過onPreviewFrame事件,捕獲圖像幀,然後壓縮,處理成byte[]然後發送啦。
不過java這裡沒有int和byte[]的轉換,很二有木有,鄙視一下java。還得自己寫代碼轉換,這裡直接生成了header[],由於沒設計其他操作,所以第一位默認0,
後4位是圖片byte[]長度。
然後依次發送數據出去。
其他的相機設置的代碼,來自baidu。
話說java這裡不熟,所以比較亂,沒有具體封裝什麼的。
C#代碼ClientInfo,一個用來保存連接的實體類,嘛~由於Demo嗎,沒仔細處理,一下也是相同原因,沒有具體優化過,不過測試過wifi條件下,傳600p左右畫質,開2~3個客戶端還是可以的。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Net.Sockets; namespace com.xwg.net { public class ClientInfo { public Thread ReceiveThread; public Socket Client; public string ip; public string name; } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Net.Sockets; using System.Net; using xwg.common; using System.IO; namespace com.xwg.net { public class SocketServer { #region 變量定義 ListclientList = null; private Socket socketServer = null; /// /// 服務器IP /// private IPAddress serverIP; ////// 監聽端口號 /// private int portNo = 15693; ////// 完整終端地址包含端口 /// private IPEndPoint serverFullAddr; // Server監聽線程 Thread accpetThread = null; #endregion #region 構造函數 public SocketServer(string ServerIP) { this.serverIP = IPAddress.Parse(ServerIP); } public SocketServer(string ServerIP, int portNo) { this.serverIP = IPAddress.Parse(ServerIP); this.portNo = portNo; } #endregion #region Event // 客戶端接入事件 public event ClientAccepted OnClientAccepted; // 連接接收數據事件 public event StreamReceived OnStreamReceived; public event ClientBreak OnClentBreak; #endregion public void StartListen() { //取得完整地址 serverFullAddr = new IPEndPoint(serverIP, portNo);//取端口號 try { // 實例化Server對象 socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 綁定監聽端口 socketServer.Bind(serverFullAddr); // 啟動監聽,制定最大20掛起 socketServer.Listen(20); } catch (Exception e) { Logger.Write("StartListen方法:"+e.Message); } clientList = new List(); accpetThread = new Thread(new ThreadStart(AcceptSocket)); accpetThread.IsBackground = true; accpetThread.Start(); } private void AcceptSocket() { while (true) { // client Socket 獲得客戶端連接 Socket acceptSock = socketServer.Accept(); Logger.Write("接收到連接!"); string ip = ((IPEndPoint)acceptSock.RemoteEndPoint).Address.ToString(); Logger.Write("客戶端IP:"); ClientInfo info = new ClientInfo(); info.Client = acceptSock; info.ip = ip; info.name = ip; Thread recThread = new Thread(new ParameterizedThreadStart(ReceiveMsg)); recThread.IsBackground = true; info.ReceiveThread = recThread; clientList.Add(info); recThread.Start(info); // 客戶端接入響應事件 if (OnClientAccepted != null) OnClientAccepted(this, info); } } private void ReceiveMsg(object obj) { ClientInfo info = (ClientInfo)obj; Socket clientSock = info.Client; try { while (true) { // 判斷連接狀態 if (!clientSock.Connected) { clientList.Remove(info); info.ReceiveThread.Abort(); clientSock.Close(); } try { byte[] header = new byte[5]; int len = clientSock.Receive(header,SocketFlags.None); if (len != 5) { // 錯誤 終端連接 Logger.Write("數據頭接收錯誤,長度不足:" + len); clientSock.Close(); info.ReceiveThread.Abort(); return; } int conLen = BitConverter.ToInt32(header, 1); //byte[] content = new byte[conLen]; //len = clientSock.Receive(content, SocketFlags.None); MemoryStream stream = new MemoryStream(); //byte [] buffer = new byte[1024]; //while ((len = clientSock.Receive(buffer)) > 0) //{ // stream.Write(buffer,0,len); //} //if (conLen != stream.Length) //{ // Logger.Write("長度錯誤:"+stream.Length+"/"+conLen); //} for (int i = 0; i < conLen; i++) { byte[] arr = new byte[1]; clientSock.Receive(arr,SocketFlags.None); stream.Write(arr,0,1); } //stream.Write(content,0,content.Length); stream.Flush(); //len = clientSock.Receive(content, SocketFlags.None); //if (len != conLen) //{ // // 錯誤 終端連接 // Logger.Write("header:" + header[1] + "," + header[2] + "," + header[3] + "," + header[4]); // Logger.Write("Content接收錯誤,長度不足:" + len+"/"+conLen); // clientSock.Close(); // return; //} // 接收事件 if (OnStreamReceived != null) { OnStreamReceived(info, stream); } } catch (Exception ex) { if (OnClentBreak != null) { OnClentBreak(this, info); } Logger.Write("Receive數據:"+ex.Message); clientSock.Close(); return; } } } catch (Exception e) { if (OnClentBreak != null) { OnClentBreak(this, info); } Logger.Write("ReceiveMsg:" + e.Message); clientSock.Close(); return; } } } public delegate void ClientAccepted(object sender,ClientInfo info); public delegate void StreamReceived(object sender,MemoryStream stream); public delegate void ClientBreak(object sender,ClientInfo info); }
簡單一說,Socket既可以做Server,也可以做Client,當然你用TCPListener也一樣效果就是了。
這裡由於是服務端,所以Socket被我Bind到了一個端口上面,啟用了Listen,監聽方法。
然後啟用了一個accpet線程,總用時實現端口監聽。
每當accpet到一個客戶端的時候,會觸發 OnClientAccepted 事件。accpet方法是會觸發阻塞的,所以絕對不可以用主線程,否則就是程序無響應。
C#處理過程中,實現封裝的最好方法就是使用事件機制了,這是我認為比Java方便的多的設計。可以把邏輯的實現,完全的拋出封裝對象。
然後就是AcceptSocket 這個方法了,這個方法當中,一旦接收到客戶端連接,會創建一個ClientInfo對象,把一些相關屬性設置上去保存。
同時新建一個線程,實現ReceiveMessage的監聽。IsBackground是一個小技巧,表示主線程結束時,把這些線程同時結束掉。比起手動結束方便多。如果你應用關閉,發現程序還在後台跑,那麼多數是由於創建的線程沒有結束的原因,這時候這個屬性會起到關鍵作用。
ReceiveMsg是接收數據的方法,這裡簡單定義了一個數據協議,數據發送時,分成兩部分發送,先發送一個5byte的header,然後是實際內容content。
header第一位表示操作標示(因為demo簡單,所以沒有具體設計協議,這個可以自定義的,也是通用的處理方法),後4位標示content內容流的長度,這樣取數據的時候,就不至於亂掉。
正常來說,Socket.Receive是有阻塞的啦,不過這裡不知道為什麼,接收的時候有問題,懷疑安卓socket流導致的,所以沒辦法直接定義buffer,一次性接受所有content,由於時間緊,沒仔細研究,反正總長度是一定不會變的,所以直接循環處理了...偷懶了有木有...
接受到信息後,通過事件OnStreamReceived 吧數據流返回出去。
服務端基本就這樣了,因為不牽扯雙向消息,所以沒處理send啦。
然後就是界面:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using com.xwg.net; using System.IO; using xwg.common; namespace MonitorServer { public partial class Form1 : Form { public Form1() { InitializeComponent(); } SocketServer server = null; Listlist = new List (); //ViewForm vf = new ViewForm(); private void button1_Click(object sender, EventArgs e) { server = new SocketServer(txtIP.Text, int.Parse(txtPort.Text)); Logger.Write("server = new SocketServer(txtIP.Text, int.Parse(txtPort.Text));"); server.OnClientAccepted += ClientAccepted; server.OnStreamReceived += StreamReceived; server.OnClentBreak += ClientBreak; server.StartListen(); Logger.Write("StartListen"); button1.Enabled = false; } public void AddClient(string ip) { try { this.Invoke((EventHandler)delegate { listBox1.Items.Add(ip); }); } catch (Exception e) { } } public void RemoveClient(string ip) { try { this.Invoke((EventHandler)delegate { listBox1.Items.Remove(ip); }); } catch (Exception e) { } } public void CloseViewForm(string ip) { try { ViewForm vf = GetViewByIP(ip); if (vf == null) return; vf.Invoke((EventHandler)delegate { vf.Close(); }); } catch (Exception e) { } } public void SetViewImage(ViewForm vf, MemoryStream stream) { try { vf.Invoke((EventHandler)delegate { Image img = Image.FromStream(stream); vf.SetImage(img); stream.Close(); }); } catch (Exception e) { } } protected void ClientAccepted(object sender, ClientInfo info) { Logger.Write("ClientAccepted:"+info.ip); AddClient(info.ip); //ViewForm vf = new ViewForm(); //list.Add(vf); //vf.SetTitle(info.ip); //vf.Show(); } private ViewForm GetViewByIP(string ip) { foreach (ViewForm vf in list) { if (vf.Text == ip) return vf; } return null; } protected void StreamReceived(object sender, MemoryStream stream) { ClientInfo info = (ClientInfo) sender; try { //Image img = Image.FromStream(stream); ////img.Save("a.jpg"); ViewForm vf = GetViewByIP(info.ip); if (vf == null) { stream.Close(); return; } SetViewImage(vf, stream); //vf.SetImage(img); //stream.Close(); } catch (Exception e) { Logger.Write("StreamReceived:"+e.Message); } } protected void ClientBreak(object sender, ClientInfo info) { CloseViewForm(info.ip); list.Remove(GetViewByIP(info.ip)); RemoveClient(info.ip); } private void listBox1_MouseDoubleClick(object sender, MouseEventArgs e) { if (listBox1.SelectedItem == null) return; string ip = listBox1.SelectedItem.ToString(); ViewForm vf = new ViewForm(); list.Add(vf); vf.SetTitle(ip); vf.Show(); } } }
然後用來展示的窗體
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace MonitorServer { public partial class ViewForm : Form { public ViewForm() { InitializeComponent(); } public void SetTitle(string title) { this.Text = title; } public void SetImage(Image img) { pictureBox1.Image = img; } } }
到此就結束了,啟動服務器應用,開始監聽,然後啟動安卓端應用,輸入服務器地址(要保證一個網絡中),連接。
服務端就會發現客戶端連接,這時,雙擊ip,就會打開預覽窗口。
支持多客戶端連接預覽,但是打開多了會卡,畢竟演示demo,沒處理優化。
實際上,應該是連接後,服務器發送命令給客戶端,客戶端才開始傳圖片流,現在沒處理,所以比較卡哦。
並且這種圖片幀傳輸的方法,雖然比較清晰,但是壓縮比小,會產生大量流量,所以只能演示用哦。
對了,還一個原因,這裡用的tcp協議,能夠保證數據包丟失重發,但是該機制會導致性能瓶頸,重發就會有時間影響哦,網絡不好,容易出現抖動等現象,其實是由於丟包引起的。這裡可以換用udp,雖然可能會出現丟幀,但是抖動現象應該會有改善,速度也會比較快。
並且這裡不支持音頻哦,如果想要完美實現的話,還是用上一篇文章提到的方法吧。
做一個控制服務器,然後實現C#客戶端和android客戶端的直連,效果應該比較好。當然,這裡也是由於spyroid這個項目,內置實現了rtsp協議服務器的原因啦。站在巨人身上總是會讓事情變得簡單。這裡感謝國外開源項目組,同時鄙視一下國內人員,百度到有用的東西,都不放出源碼,而是要收費。。。
希望對大家有用。
對了,源碼我上傳到資源裡面了,大家可以去我空間下載,包含安卓和C#後台完整項目代碼。
地址:http://download.csdn.net/detail/lanwilliam/7602669
10個資源分其實真心不高,畢竟調試了1天呢。