Android wifi机器人视频传输接收端的实现

本篇的开发基于已将S605摄像头挂载在Openwrt系统上,并且已通过网页测试访问 http://192.168.1.1:8080/?action=snapshot 摄像头视频数据流成功的基础上。
上述过程的实现,大家可以自行在论坛里查找相关资料。本篇重点讲述如何通过Android手机端实现网页端相同的视频接收功能。

首先我们需要了解到这是一个MJPEG图片视频流,是以发送逐帧的JPEG图片数据连贯起来的动画视频。Android端通过访问目标Openwrt系统上的指定服务器端口,获取这个图片流数据,并逐帧播放出来。了解此原理之后,我们的工作即可简化为在Android端创建一个socket长连接,并对目标服务器端口发送请求,并接受请求。因为数据的传输都是以字节流的形式的,所以基本传输原理与常用的socket传输方式无基本差别,主要在用字节流的解析过程,将其还原成JPEG格式的信号。

在Android的中对于视频的显示处理通常使用surfaceView这个类,继承该类实现一个自定义的surfaceView组件即可。
surfaceView有两种创建方式,一种是在Activity中正常New出来,另一种是在UI设计界面下拖拽出一个surfaceView控件,并在xml中强制转换为自定义的surfaceView类型。
下面说下这两种方式的实现区别与用途,第一种通常用于整个界面的跳转,并且new出来的surfaceView是不受UI设计界面控制的,因此无法控制其图层的顺序,并且与其他Layout控件并存等等。实现方式是直接new即可,在我们自定义的VideoSurfaceView类里,使用第一个构造函数:

1
2
3
4
public VideoSurfaceView(Context context)
{
    super(context);
}


第二种方式用于在Activity的某个区域或层级创建一个SurfaceView,并且这些都是可以通过UI设计界面控制的,实现方法是需要使用SurefaceView类的第二个构造函数

1
2
3
4
public VideoSurfaceView(Context context, AttributeSet attrs)
{
    super(context, attrs);
}

同时在LayOut中创建一个SurfaceView控件

需要通过在Layout的XML中注册才能使用,注册方法是修改SurfaceView为自定义的子类名:

1
2
3
4
5
6
<com.example.davicicontroler.VideoSurfaceView
       android:id="@+id/surfaceVideo"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginRight="0dp"
       android:layout_marginTop="0dp" /]

com.example.davicicontroler.VideoSurfaceView 注意要带上包名

由于我们需要在观测到视频信息的同时能够使用其他的控制按钮,因此这里选择使用第二种方法。
此时运行程序,Android会通过Layout的注册直接调用创建VideoSurfaceView类,而无需再额外进行new等操作。

此时我们已经将VideoSurfaceView类绑定到窗体图层的最底层了,按钮等控件悬浮于video显示区域上。当有视频信号时即可通过这个surfaceView显示出来。接下来将对VideoSurfaceView这个类进行视频信号接收的处理

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
        private SurfaceHolder surfaceHolder;    
        private Thread thread;    
        private Canvas canvas;    
        private Paint paint;    
        private int ScreenW, ScreenH;
        URL videoUrl;
        private String url;
        HttpURLConnection conn;
        Bitmap bmp;
   
        public VideoSurfaceView(Context context, AttributeSet attrs) {
            super(context, attrs);
            // TODO Auto-generated constructor stub
                    url = LoginActivity._ipEditText.getText().toString();//获取IP
            thread = new Thread(this);    
            surfaceHolder = this.getHolder();    
            surfaceHolder.addCallback(this);
            paint = new Paint();
            paint.setAntiAlias(true);    
            paint.setColor(Color.RED);
            this.setKeepScreenOn(true);// 保持屏幕常亮  
   
        }
   
       
        public void surfaceCreated(SurfaceHolder holder) { 
           
            ScreenW = this.getWidth();// 获取屏幕宽度    
            ScreenH = this.getHeight();    
            thread.start();    
        }    
        private void draw() {    
            try {    
                InputStream inputstream = null;
               
                videoUrl=new URL("http://"+ url + ":8080/?action=snapshot");   
                conn = (HttpURLConnection)videoUrl.openConnection();
                conn.setDoInput(true);
                conn.connect();
                inputstream = conn.getInputStream();
                bmp = BitmapFactory.decodeStream(inputstream);
                                Matrix matrix=new Matrix();
                            matrix.postScale(wWidth / bmp.getWidth(), wHeight / bmp.getHeight());
                            Bitmap dstbmp=Bitmap.createBitmap(bmp, 0, 0, 320, 240, matrix, true);
                canvas = surfaceHolder.lockCanvas();
                canvas.drawColor(Color.BLACK);  
                canvas.drawBitmap(dstbmp, 0, 0, paint);
                surfaceHolder.unlockCanvasAndPost(canvas);
                conn.disconnect();
            } catch (Exception ex) {    
            } finally {  
                if (canvas != null)    
                    surfaceHolder.unlockCanvasAndPost(canvas);    
            }    
        }    
        public void run() {    
            while (true) {    
                draw();    
            }    
        }
   
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                int height) {
            // TODO Auto-generated method stub
           
        }
   
        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            // TODO Auto-generated method stub
           
        }

这里简单讲解一下surfaceView的内部实现

callback接口:
当我们继承SurfaceView类并实现了SurfaceHolder.Callback接口就实现一个自定义的SurfaceView,SurfaceHolder.Callback用于在显卡底层发生变化时对自定义surfaceView类进行通知,实现图像绘制。

SurfaceHolder.Callback接口需要实现一下两个方法:
surfaceCreated(SurfaceHolder holder):初始化时调用。
surfaceChanged(SurfaceHolder holder, int format, int width,int height):当SurfaceView的状态(大小和格式)发生变化的时候会调用该函数,在surfaceCreated调用后该函数至少会被调用一次。

SurfaceHolder 类:
SurfaceView的getHolder()函数可以获取SurfaceHolder对象,Surface 就在SurfaceHolder对象内。虽然Surface保存了当前窗口的像素数据,但是在使用过程中是不直接和Surface打交道的,由SurfaceHolder的Canvas lockCanvas()或则Canvas lockCanvas()函数来获取Canvas对象,通过在Canvas上绘制内容来修改Surface中的数据。如果Surface不可编辑或则尚未创建调用该函数会返回null,在 unlockCanvas() 和 lockCanvas()中Surface的内容是不缓存的,所以需要完全重绘Surface的内容,为了提高效率只重绘变化的部分则可以调用lockCanvas(Rect rect)函数来指定一个rect区域,这样该区域外的内容会缓存起来。在调用lockCanvas函数获取Canvas后,SurfaceView会获取Surface的一个同步锁直到调用unlockCanvasAndPost(Canvas canvas)函数才释放该锁,这里的同步机制保证在Surface绘制过程中不会被改变(被摧毁、修改)。(部分描述来自网络)

matrix.postScale(wWidth / bmp.getWidth(), wHeight / bmp.getHeight());这句话的作用是创建一个使bmp放大到全屏的变化矩阵,并使用Bitmap.createBitmap(bmp, 0, 0, 320, 240, matrix, true)方法将原bmp应用这些变化后创建出一个新的图片。注意这里的320,240是原图片大小,在基于视频获取的MJPEG图像流的操作时,这个大小务必不能超过原有尺寸,否则将出现无法显示的错误。

最后,如果需要全屏显示,并去掉标题栏,可修改AndroidManifest.xml 在目标 如下:

1
2
3
4
5
6
7
8
9
10
11
         <activity
           android:name="com.example.davicicontroler.MainActivity"
           android:label="@string/app_name"
           android:theme="@android:style/Theme.NoTitleBar.Fullscreen"//这里为全屏并去掉标题栏代码
           android:screenOrientation="landscape">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
               
            </intent-filter>
        </activity>

最后传一张效果图

以上内容仅为wifi机器人视频模块的参考方案,介于传输设备及媒介的的不同,方案选择也较多。
但总体来说,传输视频的质量由摄像头及网络质量决定。
在本例中,需要注意的一个硬件问题:
由于摄像头是通过USB接口连接在wr703n路由器上的,因此,接口可能会用松动造成微小的电流变化,容易造成视频卡顿甚至掉线。因此,如果需要更进一步优化,应当选择更加牢固的连接方式。

本篇所阐述的内容大体为这些,更多内容,后期会陆续拓展。
本篇到此,谢谢关注。

BeiTown
2013.05.08

本文链接:Android wifi机器人视频传输接收端的实现

转载声明:BeiTown原创,转载请注明来源:BeiTown's Coder 编码之源,谢谢


Tags: , , , , , , ,