首先,简单介绍一下原理。主要是在手机客户端 (Android)通过实现Camera.PreviewCallback接口,在其onPreviewFrame重载函数里面获取摄像头当前图像数据, 然后通过Socket将图像数据和相关的用户名、命令等数据传输到服务器程序中。服务器端(PC端)采用C#编写,通过监听相应的端口,在获取数据后进行 相应的命令解析和图像数据还原,然后将图像数据传递至PictureBox控件中用于显示,这样就实现了手机摄像头的视频数据实时传输到服务器上。如果需 要将这些视频进行转发,通过服务器再将这些数据复制转发即可。效果如下:
对 于Android客户端上主要有几个地方需要注意,第一个就是Socket通信。Socket通信可以通过Socket类来实现,直接结合 PrintWriter来写入命令,如下定义的一个专门用于发送命令的线程类,当要连接到服务器和与服务器断开时,都需要发送命令通知服务器,此外在进行 其他文字传输时也可以采用该方法,具体代码如下:
[mw_shl_code=java,true] /**发送命令线程*/ class MySendCommondThread extends Thread{ private String commond; public MySendCommondThread(String commond){ this.commond=commond; } public void run(){ //实例化Socket try { Socket socket=new Socket(serverUrl,serverPort); PrintWriter out = new PrintWriter(socket.getOutputStream()); out.println(commond); out.flush(); } catch (UnknownHostException e) { } catch (IOException e) { } } } [/mw_shl_code]如果是采用Socket发送文件,则可以通过 OutputStream将ByteArrayInputStream数据流读入,而文件数据流则转换为ByteArrayOutputStream。如 果需要在前面添加文字,同样也需要转换为byte,然后写入OutputStream。同样也可以通过定义一个线程类发送文件,如下: [mw_shl_code=java,true] /**发送文件线程*/ class MySendFileThread extends Thread{ private String username; private String ipname; private int port; private byte byteBuffer[] = new byte[1024]; private OutputStream outsocket; private ByteArrayOutputStream myoutputstream; public MySendFileThread(ByteArrayOutputStream myoutputstream,String username,String ipname,int port){ this.myoutputstream = myoutputstream; this.username=username; this.ipname = ipname; this.port=port; try { myoutputstream.close(); } catch (IOException e) { e.printStackTrace(); } } public void run() { try{ //将图像数据通过Socket发送出去 Socket tempSocket = new Socket(ipname, port); outsocket = tempSocket.getOutputStream(); //写入头部数据信息 String msg=java.net.URLEncoder.encode("PHONEVIDEO|"+username+"|","utf-8"); byte[] buffer= msg.getBytes(); outsocket.write(buffer); ByteArrayInputStream inputstream = new ByteArrayInputStream(myoutputstream.toByteArray()); int amount; while ((amount = inputstream.read(byteBuffer)) != -1) { outsocket.write(byteBuffer, 0, amount); } myoutputstream.flush(); myoutputstream.close(); tempSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } [/mw_shl_code]而获取摄像头当前图像的关键在于onPreviewFrame() 重载函数里面,该函数里面有两个参数,第一个参数为byte[],为摄像头当前图像数据,通过YuvImage可以将该数据转换为图片文件,同时还可用对 该图片进行压缩和裁剪,将图片进行压缩转换后转换为 ByteArrayOutputStream数据,即前面发送文件线程类中所需的文件数据,然后采用线程发送文件,如下代码: [mw_shl_code=java,true] @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(); //启用线程将图像数据发送出去 Thread th = new MySendFileThread(outstream,pUsername,serverUrl,serverPort); th.start(); } } } catch (IOException e) { e.printStackTrace(); } } [/mw_shl_code]
值得注意的是,在调试中YuvImage可能找不到,在模拟机上无法执行该过程,但是编译后在真机中可以通过。此外,以上传输文字字符都是采用UTF编码,在服务器端接收时进行解析时需要采用对应的编码进行解析,否则可能会出现错误解析。
Android客户端中关键的部分主要就这些,新建一个Android项目(项目名称为SocketCamera),在main布局中添加一个SurfaceView和两个按钮,如下图所示:
然后在SocketCameraActivity.java中添加代码,具体如下:
[mw_shl_code=java,true]package com.xzy; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; 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.view.Menu; import android.view.MenuItem; 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; public class SocketCameraActivity 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.1.100"; /**服务器端口*/ private int serverPort=8888; /**视频刷新间隔*/ 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; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.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); //开始连接主机按钮 myBtn01.setOnClickListener(new OnClickListener(){ public void onClick(View v) { //Common.SetGPSConnected(LoginActivity.this, false); if(connectedServer){//停止连接主机,同时断开传输 startSendVideo=false; connectedServer=false; myBtn02.setEnabled(false); myBtn01.setText("开始连接"); myBtn02.setText("开始传输"); //断开连接 Thread th = new MySendCommondThread("PHONEDISCONNECT|"+pUsername+"|"); th.start(); } else//连接主机 { //启用线程发送命令PHONECONNECT Thread th = new MySendCommondThread("PHONECONNECT|"+pUsername+"|"); th.start(); 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{ // 开始传输视频 startSendVideo=true; myBtn02.setText("停止传输"); } }}); } @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(SocketCameraActivity.this); pUsername=preParas.getString("Username", "XZY"); serverUrl=preParas.getString("ServerUrl", "192.168.0.100"); String tempStr=preParas.getString("ServerPort", "8888"); 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(); } catch (Exception e) { e.printStackTrace(); } } @Override protected void onPause() { super.onPause(); try{ if (mCamera != null) { mCamera.setPreviewCallback(null); // !!这个必须在前,不然退出出错 mCamera.stopPreview(); mCamera.release(); mCamera = null; } } catch (Exception e) { e.printStackTrace(); } } @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.release(); 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(); //启用线程将图像数据发送出去 Thread th = new MySendFileThread(outstream,pUsername,serverUrl,serverPort); th.start(); } } } catch (IOException e) { e.printStackTrace(); } } /**创建菜单*/ public boolean onCreateOptionsMenu(Menu menu) { menu.add(0,0,0,"系统设置"); menu.add(0,1,1,"关于程序"); menu.add(0,2,2,"退出程序"); return super.onCreateOptionsMenu(menu); } /**菜单选中时发生的相应事件*/ public boolean onOptionsItemSelected(MenuItem item) { super.onOptionsItemSelected(item);//获取菜单 switch(item.getItemId())//菜单序号 { case 0: //系统设置 { Intent intent=new Intent(this,SettingActivity.class); startActivity(intent); } break; case 1://关于程序 { new AlertDialog.Builder(this) .setTitle("关于本程序") .setMessage("本程序由武汉大学水利水电学院肖泽云设计、编写。\nEmail:") .setPositiveButton ( "我知道了", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } } ) .show(); } break; case 2://退出程序 { //杀掉线程强制退出 android.os.Process.killProcess(android.os.Process.myPid()); } break; } return true; } /**发送命令线程*/ class MySendCommondThread extends Thread{ private String commond; public MySendCommondThread(String commond){ this.commond=commond; } public void run(){ //实例化Socket try { Socket socket=new Socket(serverUrl,serverPort); PrintWriter out = new PrintWriter(socket.getOutputStream()); out.println(commond); out.flush(); } catch (UnknownHostException e) { } catch (IOException e) { } } } /**发送文件线程*/ class MySendFileThread extends Thread{ private String username; private String ipname; private int port; private byte byteBuffer[] = new byte[1024]; private OutputStream outsocket; private ByteArrayOutputStream myoutputstream; public MySendFileThread(ByteArrayOutputStream myoutputstream,String username,String ipname,int port){ this.myoutputstream = myoutputstream; this.username=username; this.ipname = ipname; this.port=port; try { myoutputstream.close(); } catch (IOException e) { e.printStackTrace(); } } public void run() { try{ //将图像数据通过Socket发送出去 Socket tempSocket = new Socket(ipname, port); outsocket = tempSocket.getOutputStream(); //写入头部数据信息 String msg=java.net.URLEncoder.encode("PHONEVIDEO|"+username+"|","utf-8"); byte[] buffer= msg.getBytes(); outsocket.write(buffer); ByteArrayInputStream inputstream = new ByteArrayInputStream(myoutputstream.toByteArray()); int amount; while ((amount = inputstream.read(byteBuffer)) != -1) { outsocket.write(byteBuffer, 0, amount); } myoutputstream.flush(); myoutputstream.close(); tempSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } [/mw_shl_code]此外还有一些参数,在res/xml新建一个setting.xml文件,添加服务器地址、端口、用户名等参数设置,如下: [mw_shl_code=java,true]<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceCategory android:title="服务器设置"> <EditTextPreference android:key="Username" android:title="用户名" android:summary="用于连接服务器的用户名" android:defaultValue="XZY"/> <EditTextPreference android:key="ServerUrl" android:title="视频服务器地址" android:summary="保存服务器地址" android:defaultValue="192.168.1.100"/> <EditTextPreference android:key="ServerPort" android:title="服务器端口" android:summary="连接服务器的端口地址" android:defaultValue="8888"/> </PreferenceCategory> <PreferenceCategory android:title="视频设置"> <EditTextPreference android:key="VideoPreRate" android:title="视频刷新间隔" android:summary="设置视频刷新的间隔值,应大于等于0,值越大视频传输间隔越长" android:defaultValue="1"/> <EditTextPreference android:key="VideoQuality" android:title="图像质量" android:summary="设置图像压缩的质量,值为0~100,值越高越清晰,但同时数据也更大" android:defaultValue="85"/> <EditTextPreference android:key="VideoWidthRatio" android:title="图像宽度缩放比例" android:summary="设置图像的宽度缩放比例,值为0~100,值越高图像分辨率越高" android:defaultValue="100"/> <EditTextPreference android:key="VideoHeightRatio" android:title="图像高度缩放比例" android:summary="设置图像的高度缩放比例,值为0~100,值越高图像分辨率越高" android:defaultValue="100"/> </PreferenceCategory> </PreferenceScreen> [/mw_shl_code]
编译程序,在模拟机上效果如下:
接下来就是服务器端接收手机传输的视频数据,这与一般CS架构中服务器程序类似,主要是监听端口,然后解析数据。现新建一个C#应用程序项目(项目名称为“手机摄像头”),首先定义一些全局变量,主要包括服务器地址、端口以及相关监听对象等,如下:
// 服务器状态,如果为false表示服务器暂停,true表示服务器开启 public bool ServerStatus = false; // 服务器地址 public string ServerAddress; // 服务器端口 public int ServerPort; // 开启服务的线程 private Thread processor; // 用于TCP监听 private TcpListener tcpListener; // 与客户端连接的套接字接口 private Socket clientSocket; // 用于处理客户事件的线程 private Thread clientThread; // 手机客户端所有客户端的套接字接口 private Hashtable PhoneClientSockets = new Hashtable(); // 手机用户类数组 public ArrayList PhoneUsersArray = new ArrayList(); // 手机用户名数组 public ArrayList PhoneUserNamesArray = new ArrayList(); // 图像数据流 private ArrayList StreamArray;然后定义处理客户端传递数据的函数 ProcessClient(),主要对接收数据进行命令解析。如果是手机连接的命令("PHONECONNECT"),就在记录该套接字对象,同时在列 表中添加该对象;如果是断开连接的命令("PHONEDISCONNECT"),就移除该对象;如果是手机视频命令("PHONEVIDEO"),就分解 其包含的图像数据,如果存在该用户对应的视频窗口,就传递该图像数据到这个视频窗口中。具体代码如下: [mw_shl_code=java,true] #region 处理客户端传递数据及处理事情 /// <summary> /// 处理客户端传递数据及处理事情 /// </summary> private void ProcessClient() { Socket client = clientSocket; bool keepalive = true; while (keepalive) { Thread.Sleep(50); Byte[] buffer = null; bool tag = false; try { buffer = new Byte[1024];//client.Available int count = client.Receive(buffer, SocketFlags.None);//接收客户端套接字数据 if (count > 0)//接收到数据 tag = true; } catch (Exception e) { keepalive = false; if (client.Connected) client.Disconnect(true); client.Close(); } if (!tag) { if (client.Connected) client.Disconnect(true); client.Close(); keepalive = false; } string clientCommand = ""; try { clientCommand = System.Text.Encoding.UTF8.GetString(buffer);//转换接收的数据,数据来源于客户端发送的消息 if (clientCommand.Contains("%7C"))//从Android客户端传递部分数据 clientCommand = clientCommand.Replace("%7C", "|");//替换UTF中字符%7C为| } catch { } //分析客户端传递的命令来判断各种操作 string[] messages = clientCommand.Split('|'); if (messages != null && messages.Length > 0) { string tempStr = messages[0];//第一个字符串为命令 if (tempStr == "PHONECONNECT")//手机连接服务器 { try { string tempClientName = messages[1].Trim(); PhoneClientSockets.Remove(messages[1]);//删除之前与该用户的连接 PhoneClientSockets.Add(messages[1], client);//建立与该客户端的Socket连接 UserClass tempUser = new UserClass(); tempUser.UserName = tempClientName; tempUser.LoginTime = DateTime.Now; Socket tempSocket = (Socket)PhoneClientSockets[tempClientName]; tempUser.IPAddress = tempSocket.RemoteEndPoint.ToString(); int tempIndex = PhoneUserNamesArray.IndexOf(tempClientName); if (tempIndex >= 0) { PhoneUserNamesArray[tempIndex] = tempClientName; PhoneUsersArray[tempIndex] = tempUser; MemoryStream stream2 = (MemoryStream)StreamArray[tempIndex]; if (stream2 != null) { stream2.Close(); stream2.Dispose(); } } else//新增加 { PhoneUserNamesArray.Add(tempClientName); PhoneUsersArray.Add(tempUser); StreamArray.Add(null); } RefreshPhoneUsers(); } catch (Exception except) { } } else if (tempStr == "PHONEDISCONNECT")//某个客户端退出了 { try { string tempClientName = messages[1]; RemovePhoneUser(tempClientName); int tempPhoneIndex = PhoneUserNamesArray.IndexOf(tempClientName); if (tempPhoneIndex >= 0) { PhoneUserNamesArray.RemoveAt(tempPhoneIndex); MemoryStream memStream = (MemoryStream)StreamArray[tempPhoneIndex]; if (memStream != null) { memStream.Close(); memStream.Dispose(); } StreamArray.RemoveAt(tempPhoneIndex); } Socket tempSocket = (Socket)PhoneClientSockets[tempClientName];//第1个为客户端的ID,找到该套接字 if (tempSocket != null) { tempSocket.Close(); PhoneClientSockets.Remove(tempClientName); } keepalive = false; } catch (Exception except) { } RefreshPhoneUsers(); } else if (tempStr == "PHONEVIDEO")//接收手机数据流 { try { string tempClientName = messages[1]; string tempForeStr = messages[0] + "%7C" + messages[1] + "%7C"; int startCount = System.Text.Encoding.UTF8.GetByteCount(tempForeStr); try { MemoryStream stream = new MemoryStream(); if (stream.CanWrite) { stream.Write(buffer, startCount, buffer.Length - startCount); int len = -1; while ((len = client.Receive(buffer)) > 0) { stream.Write(buffer, 0, len); } } stream.Flush(); int tempPhoneIndex = PhoneUserNamesArray.IndexOf(tempClientName); if (tempPhoneIndex >= 0) { MemoryStream stream2 = (MemoryStream)StreamArray[tempPhoneIndex]; if (stream2 != null) { stream2.Close(); stream2.Dispose(); } StreamArray[tempPhoneIndex] = stream; PhoneVideoForm form = GetPhoneVideoForm(tempClientName); if (form != null) form.DataStream = stream; } } catch { } } catch (Exception except) { } } } else//客户端发送的命令或字符串为空,结束连接 { try { client.Close(); keepalive = false; } catch { keepalive = false; } } } }