本篇的开发基于已将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 |
第二种方式用于在Activity的某个区域或层级创建一个SurfaceView,并且这些都是可以通过UI设计界面控制的,实现方法是需要使用SurefaceView类的第二个构造函数
1 2 3 4 |
同时在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
Tags: Android, Arduino, BeiTown, MJPEG, openwrt, S605摄像头, 全屏, 视频传输