curl 下载网站图片深圳网络推广代理
一.简述
最近疏于写文,忙里抽闲写一篇关于MediaCodec使用的博文,对上一篇MediaCodec的理论讲解进行落地实现。
使用MediaCodec开发一个简易VideoPlayer,对一个视频进行解码播放的过程并不复杂。
实现过程采用的是MediaCodec的同步方式
废话不多说了,先上图阐明流程,再上代码具体实现。
二.MediaCodec解码流程
三.代码实现
简易VideoPlayer的代码文件并不多,一共也只有四个文件:
MainActivity.java:UI显示,Surface监听,视频和音频解码线程控制
VideoDecodeThread.java:视频解码线程
AudioDecodeThread.java:音频解码线程
ICodecInterface.java:用于视频/音频解码线程和MainActivity数据通讯
上代码:
res/layout/activity_main.xml
layout里不用添加什么UI控件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"></androidx.constraintlayout.widget.ConstraintLayout>
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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);SurfaceView surfaceView = new SurfaceView(this);surfaceView.getHolder().addCallback(this);setContentView(surfaceView);}@Overridepublic 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}@Overridepublic void surfaceCreated(SurfaceHolder holder) {//do nothing frist}@Overridepublic 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();}}@Overridepublic 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;}@Overridepublic 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 dataByteBuffer 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变化时,必须重新指向新的buffercase 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 nowif ((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;}@Overridepublic 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 dataByteBuffer 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}
}
src\main\java\com\android\demo\mediacodec\decode
package com.android.mediacodec;public interface ICodecInterface {void videoSizeCallBack(int width, int height);
}
四.结束
使用MediaCodec制作的一个简易视频播放器就完成了,核心的音、视频解码流程已在其中了,在此基础上可以丰富其他UI相关功能:进度条,播放/暂停,方向切换,视频播放界面放大缩小等,这些实现并不复杂,在此就不再探讨了。