自Android4.1之后增加了MediaCodec 类,通过该类可以调用系统所支持的编解码器(包括软和硬)。最近有项目需要在安卓上接收PC端推送过来的视频,实现多屏互动。其中用到了MediaCodec 。 本文简单的介绍MediaCodec 的使用,当然只是先讲解视频解码。
首先定义一个SurfaceView用来显示解码后的视频。这里简单介绍一下SurfaceView。SurfaceView是View的继承类,这个视图里内嵌了一个专门用于绘制的Surface。
使用时需要重写3个方法:
//在surface的大小发生改变时激发
(1)public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}
//在创建时激发,一般在这里调用画图的线程
(2)public void surfaceCreated(SurfaceHolder holder){}
//销毁时激发,一般在这里将画图的线程停止、释放
(3)public void surfaceDestroyed(SurfaceHolder holder) {}
SurfaceHolder是surface的控制器,用来操纵surface。通过SurfaceHolder.getSurface()可以获得该SurfaceView对象。
较详细的关于SurfaceView 可以参考 http://www.cnblogs.com/xuling/archive/2011/06/06/android.html
在public void surfaceCreated(SurfaceHolder holder){}方法里面可以初始化和配置解码器。
mMediaCodec.configure(mMediaFormat, holder.getSurface(), null, 0); mMediaCodec.start();
这样一来,解码器解码出来的视频数据就会自动输出到了SurfaceView上面,当然在 surfaceDestroyed (SurfaceHolder holder) {} 里面要记得stop掉解码器或者release掉解码器。
代码中使用了一个线程来接收PC端推送过来的数据和处理数据。当然你可以使用一个线程来接收数据,一个线程来处理数据,这样效率可能会好些。
首先是读取帧头,判断校验码和获取一帧长度。
代码如下:
InputStream is = null; byte[] header = new byte[12]; byte[] readData = new byte[4 << 19]; int len = 0; int rlen = 0; int hlen = 0; while (!isExit) { if (mClientSocket == null || mClientSocket.isClosed()) { try { Thread.sleep(300); } catch (InterruptedException e) {} continue; } try { is = mClientSocket.getInputStream(); } catch (IOException e) { closeSocket(0); <span style="white-space:pre"> </span>continue; } try { <span style="white-space:pre"> </span>len = is.read(header, 0, 12); } catch (IOException e) { } if (len <= 0) { <span style="white-space:pre"> </span>closeSocket(1); continue; } /* 校验 */ if (header[0] == (byte) 0x80 && header[1] == 0x60) { <span style="white-space:pre"> </span>// get frame length hlen = (header[8] & 0xFF); hlen += (header[9] & 0xFF) << 8; hlen += (header[10] & 0xFF) << 16; hlen += (header[11] & 0xFF) << 24; } else { closeSocket(2); continue; }
接着,再从输入流中读取一帧数据
<span style="white-space:pre"> </span>rlen = 0; // read one frame from InputStream while (hlen > 0) { try { len = is.read(readData, rlen, hlen); } catch (IOException e) { e.printStackTrace(); } if (len <= 0) { closeSocket(3); break; } hlen -= len; rlen += len; }
读取完一帧数据之后进行解码,使用的是h.264解码 video/avc。
解码的过程大概是这样:
1.获取空闲的输入缓冲区
2.向输入缓冲区喂数据
3.开始解码
4.向输出缓冲区获取解码后的数据,释放缓冲区。
int ibidx = -1; ByteBuffer[] inputBuffers = null; if (mMediaCodec != null) { try { <span style="white-space:pre"> </span>inputBuffers = mMediaCodec.getInputBuffers(); /* wait indefinitely an input buffer */ ibidx = mMediaCodec.dequeueInputBuffer(100000); } catch (IllegalStateException e) { <span style="white-space:pre"> </span>continue; } if (ibidx >= 0 ) { ByteBuffer tmp = ByteBuffer.wrap(readData, 0, rlen); inputBuffers[ibidx].clear(); inputBuffers[ibidx].put(tmp); mMediaCodec.queueInputBuffer(ibidx, 0, rlen, 0, 0); tmp.clear(); releaseOutBuf(mMediaCodec); }
dequeueInputBuffer(long timeoutUs)
方法用于申请输入缓冲区,参数timeoutUs >0 ,表示等待 timeoutUs 微妙, = 0 表示不等待 , -1 表示阻塞等待有效的输入缓冲区。 我测试时发现有些平板在这个地方总出现获取不到有效的缓冲区,而且出现的概率蛮大的,有可能是资源还没有释放完全造成的。
因为直接让解码出来的数据显示SurfaceView上面,就不需要解码出来的数据了。不过还是得释放掉缓冲区,否则解码器始终保留着。
private void releaseOutBuf(MediaCodec mc) { <span style="white-space:pre"> </span>MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); int obidx = mc.dequeueOutputBuffer(info, 0); do { <span style="white-space:pre"> </span>if (obidx == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { break; } else if (obidx == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { break; } else if (obidx > 0) { mc.releaseOutputBuffer(obidx, true); obidx = mc.dequeueOutputBuffer(info, 0); } } while (obidx > 0); }