Android:使用MediaCodec开发一个简易VideoPlayer

一.简述

最近疏于写文,忙里抽闲写一篇关于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.ConstraintLayout
    xmlns: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;

    @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相关功能:进度条,播放/暂停,方向切换,视频播放界面放大缩小等,这些实现并不复杂,在此就不再探讨了。

猜你喜欢

转载自blog.csdn.net/geyichongchujianghu/article/details/129266374