上次说到的那个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<Size> 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<VideoPreRate){ // tempPreRate++; // return; // } // tempPreRate=0; try { if(data!=null) { YuvImage image = new YuvImage(data,VideoFormatIndex, VideoWidth, VideoHeight,null); if(image!=null) { ByteArrayOutputStream outstream = new ByteArrayOutputStream(); //在此设置图片的尺寸和质量 image.compressToJpeg(new Rect(0, 0, (int)(VideoWidthRatio*VideoWidth), (int)(VideoHeightRatio*VideoHeight)), VideoQuality, outstream); outstream.flush(); SendMessage(outstream.toByteArray()); //启用线程将图像数据发送出去 //Thread th = new MySendFileThread(outstream.toByteArray()); //th.start(); } } } catch (IOException e) { e.printStackTrace(); } } Socket client = null; public void SendMessage(byte[] content) { if(client==null) return; OutputStream outsocket; try{ outsocket = client.getOutputStream(); //BufferedOutputStream dos = new BufferedOutputStream(outsocket); DataOutputStream dos = new DataOutputStream(outsocket); int len = content.length; byte[] header = Int2Byte(len); //outsocket.write(header); dos.write(header); //outsocket.flush(); //outsocket.write(content); // byte byteBuffer[] = new byte[1024]; // ByteArrayInputStream inputstream = new ByteArrayInputStream(content); // // PrintWriter pw = new PrintWriter(outsocket); // // int amount; // while ((amount = inputstream.read(byteBuffer)) != -1) { // outsocket.write(byteBuffer, 0, amount); // } //outsocket.write(content); dos.write(content); //dos.flush(); //dos.close(); //outsocket.flush(); }catch(Exception e) { e.printStackTrace(); } } public byte[] Int2Byte(int len) { byte[] rtn = new byte[5]; rtn[0] = (byte)0; rtn[1] = (byte)(len&0xff); rtn[2] = (byte)((len>>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(); } } } }
</pre><p></p><p>安卓端代码通过一个全局Socket创建连接,然后通过onPreviewFrame事件,捕获图像帧,然后压缩,处理成byte[]然后发送啦。</p><p>不过java这里没有int和byte[]的转换,很二有木有,鄙视一下java。还得自己写代码转换,这里直接生成了header[],由于没设计其他操作,所以第一位默认0,</p><p>后4位是图片byte[]长度。</p><p>然后依次发送数据出去。</p><p>其他的相机设置的代码,来自baidu。</p><p>话说java这里不熟,所以比较乱,没有具体封装什么的。</p>C#代码<p>ClientInfo,一个用来保存连接的实体类,嘛~由于Demo吗,没仔细处理,一下也是相同原因,没有具体优化过,不过测试过wifi条件下,传600p左右画质,开2~3个客户端还是可以的。</p><p></p><pre code_snippet_id="419870" snippet_file_name="blog_20140707_3_9307192" name="code" class="csharp">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; } }
服务器Socket管理类:
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<ClientInfo> clientList = null; private Socket socketServer = null; /// <summary> /// 服务器IP /// </summary> private IPAddress serverIP; /// <summary> /// 监听端口号 /// </summary> private int portNo = 15693; /// <summary> /// 完整终端地址包含端口 /// </summary> 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<ClientInfo>(); 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和多线程处理的知识。
简单一说,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<ViewForm> list = new List<ViewForm>(); //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(); } } }
界面实现了那几个事件,通过事件机制得到数据,进行UI设置。(界面嘛~就是UI层啦,一般只关注UI,不要放逻辑代码)
然后用来展示的窗体
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; } } }
其实就是绑定了一个pictureBox啦。
到此就结束了,启动服务器应用,开始监听,然后启动安卓端应用,输入服务器地址(要保证一个网络中),连接。
服务端就会发现客户端连接,这时,双击ip,就会打开预览窗口。
支持多客户端连接预览,但是打开多了会卡,毕竟演示demo,没处理优化。
实际上,应该是连接后,服务器发送命令给客户端,客户端才开始传图片流,现在没处理,所以比较卡哦。
并且这种图片帧传输的方法,虽然比较清晰,但是压缩比小,会产生大量流量,所以只能演示用哦。
对了,还一个原因,这里用的tcp协议,能够保证数据包丢失重发,但是该机制会导致性能瓶颈,重发就会有时间影响哦,网络不好,容易出现抖动等现象,其实是由于丢包引起的。这里可以换用udp,虽然可能会出现丢帧,但是抖动现象应该会有改善,速度也会比较快。
并且这里不支持音频哦,如果想要完美实现的话,还是用上一篇文章提到的方法吧。
做一个控制服务器,然后实现C#客户端和android客户端的直连,效果应该比较好。当然,这里也是由于spyroid这个项目,内置实现了rtsp协议服务器的原因啦。站在巨人身上总是会让事情变得简单。这里感谢国外开源项目组,同时鄙视一下国内人员,百度到有用的东西,都不放出源码,而是要收费。。。
希望对大家有用。
对了,源码我上传到资源里面了,大家可以去我空间下载,包含安卓和C#后台完整项目代码。
地址:http://download.csdn.net/detail/lanwilliam/7602669
10个资源分其实真心不高,毕竟调试了1天呢。
Android和C#实现实时视频传输Demo