自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);
	}