在Android中播放视频很简单,只要创建一个MediaPlayer实例,然后设置上DataSource和SurfaceView就可以了。但是播放视频还有一种方式就是使用Android提供的MediaCodec,它可以用于编码和解码。另外如果要播放使用Android Widevine加密的视频则必须使用MediaCodec来完成解密和解码的过程。MediaCodec的工作原理很好理解,如下图所示,有一个输入的ByteBuffers向其输入数据,MediaCodec进行处理后会将其输出到一个输出的ByteBuffers里,典型的生产者消费者模型。下面我们来实现一下使用MediaCodec进行解码和编码。
解码
首先我们先创建一个包装类,对MediaCodec的一些配置和控制操作给包装起来便于调用。当MediaCodec创建并配置好了之后,就需要周期性地进行releaseOutputBuffer操作输出解码后的内容到Surface。在这里我们使用Rxjava的interval操作符来进行这个周期性的操作。
public class VideoDecoder { private final Surface mSurface; private MediaCodec mDecoder; private Subscriber mSubscriber; public VideoDecoder(Surface surface) { mSurface = surface; } public void config(MediaFormat mediaFormat) { try { mDecoder = MediaCodec.createDecoderByType(Config.VIDEO_MIME); mDecoder.configure(mediaFormat, mSurface, null, 0); mDecoder.start(); } catch (IOException e) { e.printStackTrace(); } } public void config(int width, int height, ByteBuffer csd0) { Logger.i("config:" + csd0.limit()); MediaFormat format = MediaFormat.createVideoFormat(Config.VIDEO_MIME, width, height); format.setByteBuffer("csd-0", csd0); config(format); } public int dequeueInputBuffer(long timeout) { return mDecoder.dequeueInputBuffer(timeout); } public ByteBuffer getInputBuffer(int index) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { return mDecoder.getInputBuffers()[index]; } else { return mDecoder.getInputBuffer(index); } } /** * queue data to the input buffer of codec */ public void queueInputBuffer(int inIndex, int offset, int size, long presentationTimeUs, int flags) { mDecoder.queueInputBuffer(inIndex, offset, size, presentationTimeUs, flags); } /** * index to render the content to the surfaceview */ public void start() { Logger.i("index"); if (mSubscriber != null && !mSubscriber.isUnsubscribed()) { mSubscriber.unsubscribe(); } mSubscriber = new Subscriber<Boolean>() { @Override public void onCompleted() { stop(); } @Override public void on Error(Throwable e) { stop(); } @Override public void onNext(Boolean aBoolean) { if (aBoolean) { stop(); unsubscribe(); } } }; Observable.interval(Config.INTERVAL, TimeUnit.MILLISECONDS) .map(new Func1<Long, Boolean>() { @Override public Boolean call(Long aLong) { MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); int outIndex = mDecoder.dequeueOutputBuffer(info, 10000); if (outIndex > 0) { mDecoder.releaseOutputBuffer(outIndex, true); } if ((info.flags & BUFFER_FLAG_END_OF_STREAM) != 0) { Logger.d("OutputBuffer BUFFER_FLAG_END_OF_STREAM"); return true; } return false; } }) .subscribeOn(Schedulers.newThread()) .subscribe(mSubscriber); } /** * stop mFileDecoder */ public void stop() { Logger.e("stop"); if (mSubscriber != null && !mSubscriber.isUnsubscribed()) { mSubscriber.unsubscribe(); } if (mDecoder != null) { mDecoder.stop(); mDecoder.release(); } } }
解码的过程还需要同MediaExtractor结合起来,根据mime type 从MediaExtractor中取出一条track,可以是video也可以是audio, 然后根据这条track的MediaFormat来对MediaCodec进行配置就完成了准备阶段。然后不断地从MediaExtractor中取出Sample数据,将其填充到输入buffer中。这个过程我们使用了Rxjava来完成,一方面逻辑清晰,另一方面可以让我们很容易地控制填充buffer的速度。代码如下:
Observable.range(0, mMediaExtractor.getTrackCount()) .filter(new Func1<Integer, Boolean>() { @Override public Boolean call(Integer integer) { //find the video track MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(integer); String mime = mediaFormat.getString(MediaFormat.KEY_MIME); Logger.d(mime); return mime.startsWith("video"); } }) .flatMap(new Func1<Integer, Observable<Long>>() { @Override public Observable<Long> call(Integer integer) { //create mFileDecoder according the video track MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(integer); mMediaExtractor.selectTrack(integer); mDecoder.config(mediaFormat); return Observable.interval(Config.INTERVAL, TimeUnit.MILLISECONDS); } }) .map(new Func1<Long, Boolean>() { @Override public Boolean call(Long aLong) { int inIndex = mDecoder.dequeueInputBuffer(10000); if (inIndex >= 0) { ByteBuffer buffer = mDecoder.getInputBuffer(inIndex); int sampleSize = mMediaExtractor.readSampleData(buffer, 0); if (sampleSize < 0) { Logger.d("Input buffer eos"); mDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); return true; } else { mDecoder.queueInputBuffer(inIndex, 0, sampleSize, mMediaExtractor.getSampleTime(), 0); mMediaExtractor.advance(); } } return false; } }) .subscribe(mSubscriber);
编码
编码同解码一样,还是一个输入-处理-输出的过程。通过下面的方法,我们可以得到一个用来作为输入的Surface:
mSurface = mCodec.createInputSurface();
得到这个Surface之后,我们首先需要通过lockCanvas获得一个Canvas。 有了Canvas,我们就可以在上面画任何我们想画的东西了, 如画一些圆。
Canvas canvas = mSurface.lockCanvas(null); try { onDraw(canvas); } finally { mSurface.unlockCanvasAndPost(canvas); } void onDraw(Canvas canvas) { canvas.drawColor(Color.BLUE); if (mPaint == null) { mPaint = new TextPaint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.YELLOW); } canvas.drawCircle(OUTPUT_WIDTH / 2, OUTPUT_HEIGHT / 2, currentRadius, mPaint); currentRadius += 10; currentRadius = currentRadius > 100 ? 10 : currentRadius; }
在这个Canvas上画的内容会传输MediaCodec进行处理,然后会将编码后的内容输出到MediaCodec的显示Surface上。这个过程需要我们对输入和输出的buffer做一些处理,如输出了一定长度的buffer并release之后,我们就可以通知输入的buffer来输入同样长度的内容。也就是说输入和输出的速度是由我们来控制的。
int status = mCodec.dequeueOutputBuffer(mBufferInfo, 10000); if (status >= 0) { // encoded sample ByteBuffer data = mCodec.getOutputBuffer(status); if (data != null) { final int endOfStream = mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM; // pass to whoever listens to if (endOfStream == 0 && mLister != null) { mLister.onSampleEncoded(mBufferInfo, data); } // releasing buffer isimport ant mCodec.releaseOutputBuffer(status, false); if (endOfStream == MediaCodec.BUFFER_FLAG_END_OF_STREAM) return true; } } public void onSampleEncoded(MediaCodec.BufferInfo info, ByteBuffer data) { Logger.v("onSample encoded"); if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) { mDecoder.config(OUTPUT_WIDTH, OUTPUT_HEIGHT, data); mDecoder.start(); } else { int inIndex = mDecoder.dequeueInputBuffer(10000); if (inIndex >= 0) { ByteBuffer buffer = mDecoder.getInputBuffer(inIndex); buffer.put(data); if (info.size < 0) { Logger.d("Input buffer eos"); mDecoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); } else { mDecoder.queueInputBuffer(inIndex, 0, info.size, info.presentationTimeUs, info.flags); } } } }
最终我们就可以在输出的SurfaceView上看到不断重复画的圆了。
本文中的源代码在 github 上