首先,简单介绍一下原理。主要是在手机客户端(Android)通过实现Camera.PreviewCallback接口,在其onPreviewFrame重载函数里面获取摄像头当前图像数据,然后通过Socket将图像数据和相关的用户名、命令等数据传输到服务器程序中。服务器端(PC端)采用C#编写,通过监听相应的端口,在获取数据后进行相应的命令解析和图像数据还原,然后将图像数据传递至PictureBox控件中用于显示,这样就实现了手机摄像头的视频数据实时传输到服务器上。如果需要将这些视频进行转发,通过服务器再将这些数据复制转发即可。效果如下:

对于Android客户端上主要有几个地方需要注意,第一个就是Socket通信。Socket通信可以通过Socket类来实现,直接结合PrintWriter来写入命令,如下定义的一个专门用于发送命令的线程类,当要连接到服务器和与服务器断开时,都需要发送命令通知服务器,此外在进行其他文字传输时也可以采用该方法,具体代码如下:
-
- class MySendCommondThread extends Thread{
- private String commond;
- public MySendCommondThread(String commond){
- this.commond=commond;
- }
- public void run(){
-
- try {
- Socket socket=new Socket(serverUrl,serverPort);
- PrintWriter out = new PrintWriter(socket.getOutputStream());
- out.println(commond);
- out.flush();
- } catch (UnknownHostException e) {
- } catch (IOException e) {
- }
- }
- }
如果是采用Socket发送文件,则可以通过OutputStream将ByteArrayInputStream数据流读入,而文件数据流则转换为ByteArrayOutputStream。如果需要在前面添加文字,同样也需要转换为byte,然后写入OutputStream。同样也可以通过定义一个线程类发送文件,如下:
-
- 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 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();
- }
- }
- }
而获取摄像头当前图像的关键在于onPreviewFrame()重载函数里面,该函数里面有两个参数,第一个参数为byte[],为摄像头当前图像数据,通过YuvImage可以将该数据转换为图片文件,同时还可用对该图片进行压缩和裁剪,将图片进行压缩转换后转换为 ByteArrayOutputStream数据,即前面发送文件线程类中所需的文件数据,然后采用线程发送文件,如下代码:
- @Override
- public void onPreviewFrame(byte[] data, Camera camera) {
-
-
- 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();
- }
- }
值得注意的是,在调试中YuvImage可能找不到,在模拟机上无法执行该过程,但是编译后在真机中可以通过。此外,以上传输文字字符都是采用UTF编码,在服务器端接收时进行解析时需要采用对应的编码进行解析,否则可能会出现错误解析。
Android客户端中关键的部分主要就这些,新建一个Android项目(项目名称为SocketCamera),在main布局中添加一个SurfaceView和两个按钮,如下图所示:
然后在SocketCameraActivity.java中添加代码,具体如下:
- 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;
- private SurfaceHolder mSurfaceHolder = null;
- private Camera mCamera = null;
-
-
- 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;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
-
-
- 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) {
-
- if(connectedServer){
- startSendVideo=false;
- connectedServer=false;
- myBtn02.setEnabled(false);
- myBtn01.setText("开始连接");
- myBtn02.setText("开始传输");
-
- Thread th = new MySendCommondThread("PHONEDISCONNECT|"+pUsername+"|");
- th.start();
- }
- else
- {
-
- 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();
- mSurfaceHolder.addCallback(this);
- mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
-
- 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) {
-
- 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) {
-
- try {
- if (mCamera != null) {
- mCamera.setPreviewDisplay(mSurfaceHolder);
- mCamera.startPreview();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void surfaceDestroyed(SurfaceHolder holder) {
-
- if (null != mCamera) {
- mCamera.setPreviewCallback(null);
- mCamera.stopPreview();
- mCamera.release();
- mCamera = null;
- }
- }
-
- @Override
- public void onPreviewFrame(byte[] data, Camera camera) {
-
-
- 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:[email protected]")
- .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(){
-
- 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 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();
- }
- }
- }
- }
此外还有一些参数,在res/xml新建一个setting.xml文件,添加服务器地址、端口、用户名等参数设置,如下:
- <?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>
编译程序,在模拟机上效果如下:
接下来就是服务器端接收手机传输的视频数据,这与一般CS架构中服务器程序类似,主要是监听端口,然后解析数据。现新建一个C#应用程序项目(项目名称为“手机摄像头”),首先定义一些全局变量,主要包括服务器地址、端口以及相关监听对象等,如下:
-
-
-
- public bool ServerStatus = false;
-
-
-
- public string ServerAddress;
-
-
-
- public int ServerPort;
-
-
-
- private Thread processor;
-
-
-
- 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"),就分解其包含的图像数据,如果存在该用户对应的视频窗口,就传递该图像数据到这个视频窗口中。具体代码如下:
关于开启服务监听、刷新用户列表、获取手机视频窗体、删除用户、寻找用户序号等代码在此就不详细介绍,具体参见源代码。
基于Socket的Android手机视频实时传输手机客户端下载地址:
http://download.csdn.net/detail/xwebsite/4973592
基于Socket的Android手机视频实时传输服务器端下载地址:
http://download.csdn.net/detail/xwebsite/4973601
基于Socket的Android手机视频实时传输所有源程序下载地址:
http://download.csdn.net/detail/xwebsite/4973613