MediaCodec
MediaCodec的介绍
MediaCodec是Android平台提供的硬件编解码器,它由一组API构成。这样说还是太抽象了,下面用一个很简单的开源例子MediaCodecDemo来说明MediaCodec怎么使用。
MediaCodecDemo:https://github.com/vecio/MediaCodecDemo
MediaCodec的实例
MediaCodecDemo的源代码文件只有一个,比较简单,主要的功能是读取一个本地的MP4文件,然后对它进行解码,最后显示在屏幕上
1、创建显示区域SurfaceView
2、把显示区域绑定到播放线程上
3、创建解复用器MediaExtractor,它的功能是分离音频和视频
4、设置解复用器的数据源(即MP4文件的路径)
5、遍历数据源的track的信息,track是MP4文件中的数据的集合,分析该track的格式,因为我们的目的是处理视频,因此,我们需要找到视频所在的track
6、找到视频所在的track后,让解复用器选择它,表示接下来我们要处理这个track;然后根据格式信息创建解码器MediaCodec
7、对解码器进行配置
8、启动解码器
9、得到解码器的输入队列和输出队列;队列里面存放的是一个一个的缓冲区;把缓冲区组成队列的目的是为了复用这里缓冲区,例如,一个缓冲区用完之后,可以把它放进队列尾部,让解码器重新使用;输入队列中的缓冲区用于存放从MP4文件读取的数据;输出队列的缓冲区存放的是已经解码的数据
10、进入循环
(1)获取一个空闲的输入缓冲区,然后使用解复用器读取数据,放进缓冲区中,再把这个缓冲区添加到解码器的输入队列的尾部,让解码器进行解码
(2)获取一个有效的输出缓冲区,这个缓冲区存放的是已经解码完成的数据
(3)解码器内部会把解码的数据显示在SurfaceView上
完整的代码如下:
package io.vec.demo.mediacodec;
import java.nio.ByteBuffer;
import android.app.Activity;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class DecodeActivity extends Activity implements SurfaceHolder.Callback {
// MP4文件的路径
private static final String SAMPLE = Environment.getExternalStorageDirectory() + "/video.mp4";
// 播放线程
private PlayerThread mPlayer = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SurfaceView sv = new SurfaceView(this);
sv.getHolder().addCallback(this);
setContentView(sv);
}
protected void onDestroy() {
super.onDestroy();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mPlayer == null) {
mPlayer = new PlayerThread(holder.getSurface());
mPlayer.start();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mPlayer != null) {
mPlayer.interrupt();
}
}
/*
** 播放线程
** 从文件中读取媒体数据,然后解码,再显示到屏幕上
*/
private class PlayerThread extends Thread {
private MediaExtractor extractor; // 解复用器,将audio和video分离
private MediaCodec decoder; // 解码器
private Surface surface; // 显示区域
public PlayerThread(Surface surface) {
this.surface = surface;
}
@Override
public void run() {
extractor = new MediaExtractor();
// 设置样本源,就是文件名
extractor.setDataSource(SAMPLE);
// 读取MP4中track(track是样本的集合,也就是数据),注意和trak box的不同
for (int i = 0; i < extractor.getTrackCount(); i++) {
// 媒体格式
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
// 找到视频
if (mime.startsWith("video/")) {
extractor.selectTrack(i);
// 创建解码器
decoder = MediaCodec.createDecoderByType(mime);
// 设置解码数据显示的地方
decoder.configure(format, surface, null, 0);
break;
}
}
if (decoder == null) {
Log.e("DecodeActivity", "Can't find video info!");
return;
}
// 开始解码
decoder.start();
// 输入缓存区数组,就是放置未解码数据的地方
ByteBuffer[] inputBuffers = decoder.getInputBuffers();
// 输出缓存区数组,放置已经解码的数据的地方
ByteBuffer[] outputBuffers = decoder.getOutputBuffers();
BufferInfo info = new BufferInfo();
boolean isEOS = false;
long startMs = System.currentTimeMillis();
while (!Thread.interrupted()) {
if (!isEOS) {
// 取得一个空闲的输入缓存区的索引,因为输入缓存区是以队列的方式被重复数据,出队的是空闲的缓存区
int inIndex = decoder.dequeueInputBuffer(10000);
if (inIndex >= 0) {
// 根据索引得到输入缓存区
ByteBuffer buffer = inputBuffers[inIndex];
// 读取数据
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
// We shouldn't stop the playback at this point, just pass the EOS
// flag to decoder, we will get it again from the
// dequeueOutputBuffer
Log.d("DecodeActivity", "InputBuffer BUFFER_FLAG_END_OF_STREAM");
decoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isEOS = true;
} else {
// 数据入队
decoder.queueInputBuffer(inIndex, 0, sampleSize, extractor.getSampleTime(), 0);
// 移动到下一帧
extractor.advance();
}
}
}
// 读取解码的输出
int outIndex = decoder.dequeueOutputBuffer(info, 10000);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: // 输出缓冲区已经改变
Log.d("DecodeActivity", "INFO_OUTPUT_BUFFERS_CHANGED");
outputBuffers = decoder.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: // 格式已经更改
Log.d("DecodeActivity", "New format " + decoder.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER: // 请重试
Log.d("DecodeActivity", "dequeueOutputBuffer timed out!");
break;
default:
// 得到输出缓冲区,就是一帧
ByteBuffer buffer = outputBuffers[outIndex];
Log.v("DecodeActivity", "We can't use this buffer but render it due to the API limit, " + buffer);
// We use a very simple clock to keep the video FPS, or the video
// playback will be too fast
// 休眠一段时间
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
// 输出数据已经使用完毕,那么可以释放它,这样解码器就可以重复使用它了
decoder.releaseOutputBuffer(outIndex, true);
break;
}
// All decoded frames have been rendered, we can stop playing now
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d("DecodeActivity", "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
break;
}
}
// 解码器停止
decoder.stop();
// 释放解码器
decoder.release();
// 释放流复用器
extractor.release();
}
}
}
下面按照步骤进行详细讲解。
创建显示区域SurfaceView
// 创建SurfaceView
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SurfaceView sv = new SurfaceView(this);
sv.getHolder().addCallback(this);
setContentView(sv);
}
把显示区域绑定到播放线程上
// 显示区域和解码线程绑定
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mPlayer == null) {
mPlayer = new PlayerThread(holder.getSurface());
mPlayer.start();
}
}
创建解复用器
解复用器的作用是分离音频和视频
// 创建解复用器
extractor = new MediaExtractor();
// 设置样本源,就是文件名
extractor.setDataSource(SAMPLE);
根据视频媒体的格式创建解码器
遍历解复用器的Track(Track是数据样本的集合),选择视频样本,然后取得媒体格式,根据媒体格式创建解码器
// 读取MP4中track(track是样本的集合,也就是数据),注意和trak box的不同
for (int i = 0; i < extractor.getTrackCount(); i++) {
// 媒体格式
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
// 找到视频
if (mime.startsWith("video/")) {
extractor.selectTrack(i);
// 创建解码器
decoder = MediaCodec.createDecoderByType(mime);
// 设置解码数据显示的地方
decoder.configure(format, surface, null, 0);
break;
}
}
启动解码器
// 开始解码
decoder.start();
获取空闲缓冲区然后读取MP4数据
这些数据是压缩的数据
if (!isEOS) {
// 取得一个空闲的输入缓存区的索引,因为输入缓存区是以队列的方式被重复数据,出队的是空闲的缓存区
int inIndex = decoder.dequeueInputBuffer(10000);
if (inIndex >= 0) {
// 根据索引得到输入缓存区
ByteBuffer buffer = inputBuffers[inIndex];
// 读取数据
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
// We shouldn't stop the playback at this point, just pass the EOS
// flag to decoder, we will get it again from the
// dequeueOutputBuffer
Log.d("DecodeActivity", "InputBuffer BUFFER_FLAG_END_OF_STREAM");
decoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isEOS = true;
} else {
// 数据入队
decoder.queueInputBuffer(inIndex, 0, sampleSize, extractor.getSampleTime(), 0);
// 移动到下一帧
extractor.advance();
}
}
}
取得解码器的输出
这些数据是解压后的数据,可以用来显示
// 读取解码的输出
int outIndex = decoder.dequeueOutputBuffer(info, 10000);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: // 输出缓冲区已经改变
Log.d("DecodeActivity", "INFO_OUTPUT_BUFFERS_CHANGED");
outputBuffers = decoder.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: // 格式已经更改
Log.d("DecodeActivity", "New format " + decoder.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER: // 请重试
Log.d("DecodeActivity", "dequeueOutputBuffer timed out!");
break;
default:
// 得到输出缓冲区,就是一帧
ByteBuffer buffer = outputBuffers[outIndex];
Log.v("DecodeActivity", "We can't use this buffer but render it due to the API limit, " + buffer);
// We use a very simple clock to keep the video FPS, or the video
// playback will be too fast
// 休眠一段时间
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
// 输出数据已经使用完毕,那么可以释放它,这样解码器就可以重复使用它了
decoder.releaseOutputBuffer(outIndex, true);
break;
}