上次說到的那個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();
List list = 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 變量定義
List clientList = 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;
List list = 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天呢。