当前位置: 代码迷 >> Android >> Android跟C#实现实时视频传输Demo
  详细解决方案

Android跟C#实现实时视频传输Demo

热度:54   发布时间:2016-04-28 04:56:05.0
Android和C#实现实时视频传输Demo

上次说到的那个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天呢。

  相关解决方案