最近疏于写文,忙里抽闲写一篇关于MediaCodec使用的博文,对上一篇MediaCodec的理论讲解进行落地实现。
使用MediaCodec开发一个简易VideoPlayer,对一个视频进行解码播放的过程并不复杂。
实现过程采用的是MediaCodec的同步方式
废话不多说了,先上图阐明流程,再上代码具体实现。
简易VideoPlayer的代码文件并不多,一共也只有四个文件:
MainActivity.java:UI显示,Surface监听,视频和音频解码线程控制
VideoDecodeThread.java:视频解码线程
AudioDecodeThread.java:音频解码线程
ICodecInterface.java:用于视频/音频解码线程和MainActivity数据通讯
res/layout/activity_main.xml
layout里不用添加什么UI控件
src\main\java\com\android\mediacodec\MainActivity.java
package com.android.mediacodec;
import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
* MediaCodec VideoPlayer Example
*
* @author Shawn
*/
public class MainActivity extends Activity implements SurfaceHolder.Callback, ICodecInterface {
private static final String TAG = "MainActivity";
//路径目前是写死的,需要在手机根目录下先拷贝一个名字为mediatest的mp4视频文件
private static final String FILE_PATH = Environment.getExternalStorageDirectory() + "/mediatest.mp4";
private VideoDecodeThread mVideoDecodeThread;
private AudioDecodeThread mAudioDecodeThread;
private int mVideoWidth, mVideoHeight;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SurfaceView surfaceView = new SurfaceView(this);
surfaceView.getHolder().addCallback(this);
setContentView(surfaceView);
}
@Override
public void changeVideoSize(int width, int height) {
Log.v(TAG, "VideosizeCallBack() width:" + width + " x " + "height:" + height);
mVideoWidth = width;
mVideoHeight = height;
}
//改变视频的尺寸自适应。
public void changeVideoSize() {
//do nothing frist
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
//do nothing frist
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (mVideoDecodeThread == null) {
mVideoDecodeThread = new VideoDecodeThread(holder.getSurface(), FILE_PATH);
mVideoDecodeThread.setPlayStateListener(this);
mVideoDecodeThread.start();
}
if (mAudioDecodeThread == null) {
mAudioDecodeThread = new AudioDecodeThread(FILE_PATH);
mAudioDecodeThread.start();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mVideoDecodeThread != null) {
mVideoDecodeThread.interrupt();
}
if (mAudioDecodeThread != null) {
mAudioDecodeThread.interrupt();
}
}
/**
* 关闭线程
*/
public void destroy() {
if (mVideoDecodeThread != null) {
mVideoDecodeThread.close();
}
if (mAudioDecodeThread != null) {
mAudioDecodeThread.close();
}
}
}
src\main\java\com\android\mediacodec\VideoDecodeThread.java
package com.android.mediacodec;
import java.io.IOException;
import java.nio.ByteBuffer;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;
public class VideoDecodeThread extends Thread {
private static final String TAG = "VideoDecoderThread";
private MediaExtractor mVideoExtractor;
private MediaCodec mVideoDecoder;
private Surface mSurface;
private String mFilePath;
private ICodecInterface mListener;
private static final long TIMEOUT_US = 10000;
private static final String VIDEO = "video/";
public VideoDecodeThread(Surface surface, String filePath) {
mSurface = surface;
mFilePath = filePath;
}
/**
* 设置回调
* @param listener
*/
public void setPlayStateListener(ICodecInterface listener) {
mListener = listener;
}
@Override
public void run() {
mVideoExtractor = new MediaExtractor();
try {
mVideoExtractor.setDataSource(mFilePath);
} catch (IOException e) {
e.printStackTrace();
}
//获取视频所在轨道
for (int i = 0; i < mVideoExtractor.getTrackCount(); i++) {
MediaFormat mediaformat = mVideoExtractor.getTrackFormat(i);
//Log.d(TAG, "getTrackCount: " + mExtractor.getTrackCount());
String mime = mediaformat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith(VIDEO)) {
MediaFormat format = mVideoExtractor.getTrackFormat(i);
Log.d(TAG, "format : " + format);
int width = mediaformat.getInteger(MediaFormat.KEY_WIDTH);
int height = mediaformat.getInteger(MediaFormat.KEY_HEIGHT);
float time = mediaformat.getLong(MediaFormat.KEY_DURATION) / 1000000;
mListener.changeVideoSize(width, height);
mVideoExtractor.selectTrack(i);
try {
mVideoDecoder = MediaCodec.createDecoderByType(mime);
mVideoDecoder.configure(format, mSurface, null, 0 /* Decoder */);
} catch (IOException e) {
Log.e(TAG, "codec '" + mime + "' failed configuration. " + e);
return;
}
}
}
if (mVideoDecoder == null) {
Log.e(TAG, "video decoder is unexpectedly null");
return;
}
mVideoDecoder.start();
BufferInfo videoBufferInfo = new BufferInfo();
ByteBuffer[] inputBuffers = mVideoDecoder.getInputBuffers();
//mVideoDecoder.getOutputBuffers();
boolean eosReceived = false;
long startTime = System.currentTimeMillis();
while (!Thread.interrupted()) {
//将资源传递到解码器
if (!eosReceived) {
int inputIndex = mVideoDecoder.dequeueInputBuffer(TIMEOUT_US);
if (inputIndex >= 0) {
// fill inputBuffers[inputBufferIndex] with valid data
ByteBuffer inputBuffer = inputBuffers[inputIndex];
int sampleSize = mVideoExtractor.readSampleData(inputBuffer, 0);
if (sampleSize > 0) {
mVideoDecoder.queueInputBuffer(inputIndex, 0, sampleSize, mVideoExtractor.getSampleTime(), 0);
mVideoExtractor.advance();
} else {
Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
mVideoDecoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
eosReceived = true;
}
}
}
int outIndex = mVideoDecoder.dequeueOutputBuffer(videoBufferInfo, TIMEOUT_US);
switch (outIndex) {
//当buffer变化时,必须重新指向新的buffer
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
//mVideoDecoder.getOutputBuffers();
break;
//当Buffer的封装格式发生变化的时候,需重新指向新的buffer格式
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.d(TAG, "INFO_OUTPUT_FORMAT_CHANGED format : " + mVideoDecoder.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.d(TAG, "INFO_TRY_AGAIN_LATER");
break;
default:
while ((videoBufferInfo.presentationTimeUs / 1000) > (System.currentTimeMillis() - startTime)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
mVideoDecoder.releaseOutputBuffer(outIndex, true /* Surface init */);
break;
}
// All decoded frames have been rendered, we can stop playing now
if ((videoBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
break;
}
}
mVideoDecoder.stop();
mVideoDecoder.release();
mVideoExtractor.release();
}
public void close() {
//do nothing frist
}
}
src\main\java\com\android\mediacodec\AudioDecodeThread.java
解码视频的同时也需要解码音频,要不然播放就会没有声音
要注意一点的是音频解码与视频的时间同步,否则就会出现声音和画面对不上的现象
package com.android.mediacodec;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;
import java.io.IOException;
import java.nio.ByteBuffer;
public class AudioDecodeThread extends Thread {
private static final String TAG = "AudioDecodeThread";
MediaExtractor mAudioExtractor = new MediaExtractor();
MediaCodec mAudioDecoder = null;
private int mInputBufferSize;
private AudioTrack mAudioTrack;
private String mFilePath;
private static final String AUDIO = "audio/";
private static final long TIMEOUT_US = 10000;
//private boolean isAudioEOS = false;
public AudioDecodeThread(String filePath) {
mFilePath = filePath;
}
@Override
public void run() {
try {
mAudioExtractor.setDataSource(mFilePath);
} catch (IOException e) {
e.printStackTrace();
}
for (int i = 0; i < mAudioExtractor.getTrackCount(); i++) {
MediaFormat mediaFormat = mAudioExtractor.getTrackFormat(i);
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
if (mime.startsWith(AUDIO)) {
//设置音轨
mAudioExtractor.selectTrack(i);
int audioChannels = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int audioSampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int minBufferSize = AudioTrack.getMinBufferSize(audioSampleRate,
(audioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO),
AudioFormat.ENCODING_PCM_16BIT);
int maxInputSize = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
mInputBufferSize = minBufferSize > 0 ? minBufferSize * 4 : maxInputSize;
int frameSizeInBytes = audioChannels * 2;
mInputBufferSize = (mInputBufferSize / frameSizeInBytes) * frameSizeInBytes;
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
audioSampleRate,
(audioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO),
AudioFormat.ENCODING_PCM_16BIT,
mInputBufferSize,
AudioTrack.MODE_STREAM);
mAudioTrack.play();
try {
mAudioDecoder = MediaCodec.createDecoderByType(mime);
mAudioDecoder.configure(mediaFormat, null, null, 0);
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
if (mAudioDecoder == null) {
Log.e(TAG, "audio decoder is unexpectedly null");
return;
}
mAudioDecoder.start();
final ByteBuffer[] buffers = mAudioDecoder.getOutputBuffers();
int sz = buffers[0].capacity();
if (sz <= 0) {
sz = mInputBufferSize;
}
byte[] mAudioOutTempBuf = new byte[sz];
BufferInfo audioBufferInfo = new BufferInfo();
ByteBuffer[] inputBuffers = mAudioDecoder.getInputBuffers();
ByteBuffer[] outputBuffers = mAudioDecoder.getOutputBuffers();
long startMs = System.currentTimeMillis();
boolean eosReceived = false;
while (!Thread.interrupted()) {
if (!eosReceived) {
int inputIndex = mAudioDecoder.dequeueInputBuffer(TIMEOUT_US);
if (inputIndex >= 0) {
// fill inputBuffers[inputBufferIndex] with valid data
ByteBuffer inputBuffer = inputBuffers[inputIndex];
int sampleSize = mAudioExtractor.readSampleData(inputBuffer, 0);
if (sampleSize > 0) {
mAudioDecoder.queueInputBuffer(inputIndex, 0, sampleSize, mAudioExtractor.getSampleTime(), 0);
mAudioExtractor.advance();
} else {
Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
mAudioDecoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
eosReceived = true;
}
}
}
// 获取解码后的数据
int outputBufferIndex = mAudioDecoder.dequeueOutputBuffer(audioBufferInfo, TIMEOUT_US);
switch (outputBufferIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.d(TAG, "INFO_OUTPUT_FORMAT_CHANGED");
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.d(TAG, "INFO_TRY_AGAIN_LATER");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
outputBuffers = mAudioDecoder.getOutputBuffers();
Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
break;
default:
ByteBuffer outputBuffer = mAudioDecoder.getOutputBuffer(outputBufferIndex); //outputBuffers[outputBufferIndex]; //SDK<21使用
// 延时解码,跟视频时间同步
while ((audioBufferInfo.presentationTimeUs / 1000) > (System.currentTimeMillis() - startMs)) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
// 如果解码成功,则将解码后的音频PCM数据用AudioTrack播放出来
if (audioBufferInfo.size > 0) {
if (mAudioOutTempBuf.length < audioBufferInfo.size) {
mAudioOutTempBuf = new byte[audioBufferInfo.size];
}
outputBuffer.position(0);
outputBuffer.get(mAudioOutTempBuf, 0, audioBufferInfo.size);
outputBuffer.clear();
if (mAudioTrack != null)
mAudioTrack.write(mAudioOutTempBuf, 0, audioBufferInfo.size);
}
// 释放资源
mAudioDecoder.releaseOutputBuffer(outputBufferIndex, false);
break;
}
}
mAudioDecoder.stop();
mAudioDecoder.release();
mAudioExtractor.release();
mAudioTrack.stop();
mAudioTrack.release();
}
public void close() {
//do nothing frist
}
}
使用MediaCodec制作的一个简易视频播放器就完成了,核心的音、视频解码流程已在其中了,在此基础上可以丰富其他UI相关功能:进度条,播放/暂停,方向切换,视频播放界面放大缩小等,这些实现并不复杂,在此就不再探讨了。