自Android4.1之后增加了MediaCodec 类,通过该类可以调用系统所支持的编解码器(包括软和硬)。最近有项目需要在安卓上接收PC端推送过来的视频,实现多屏互动。其中用到了MediaCodec 。 本文简单的介绍MediaCodec 的使用,当然只是先讲解视频解码。
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 可以参考
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 > </span>continue;
}
try {
<span > </span>len = is.read(header, 0, 12);
} catch (IOException e) {
}
if (len <= 0) {
<span > </span>closeSocket(1);
continue;
}
/* 校验 */
if (header[0] == (byte) 0x80 && header[1] == 0x60) {
<span > </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 > </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。
int ibidx = -1;
ByteBuffer[] inputBuffers = null;
if (mMediaCodec != null) {
try {
<span > </span>inputBuffers = mMediaCodec.getInputBuffers();
/* wait indefinitely an input buffer */
ibidx = mMediaCodec.dequeueInputBuffer(100000);
} catch (IllegalStateException e) {
<span > </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 > </span>MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int obidx = mc.dequeueOutputBuffer(info, 0);
do {
<span > </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);
}