android studio 使用 mediacodec 解碼 h264 文件

參考了 https://www.jianshu.com/p/0695891fa834

首先 封裝 mediacodec,針對 H264 進行解碼

/**
 * Created by ZhangHao on 2016/8/5.
 * 用於硬件解碼(MediaCodec)H264的工具類
 */
public class MediaCodecUtil {
    //自定義的log打印,可以無視
    Logger logger = Logger.getLogger();

    private String TAG = "MediaCodecUtil";
    //解碼後顯示的surface及其寬高
    private SurfaceHolder holder;
    private int width, height;
    //解碼器
    private MediaCodec mCodec;
    private boolean isFirst = true;
    // 需要解碼的類型
    private final static String MIME_TYPE = "video/avc"; // H.264 Advanced Video
    private final static int TIME_INTERNAL = 5;

    /**
     * 初始化解碼器
     *
     * @param holder 用於顯示視頻的surface
     * @param width  surface寬
     * @param height surface高
     */
    public MediaCodecUtil(SurfaceHolder holder, int width, int height) {
//        logger.d("MediaCodecUtil() called with: " + "holder = [" + holder + "], " +
//                "width = [" + width + "], height = [" + height + "]");
        this.holder = holder;
        this.width = width;
        this.height = height;
    }

    public MediaCodecUtil(SurfaceHolder holder) {
        this(holder, holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height());
    }

    public void startCodec() {
        if (isFirst) {
            //第一次打開則初始化解碼器
            initDecoder();
        }
    }

    private void initDecoder() {
        try {
            //根據需要解碼的類型創建解碼器
            mCodec = MediaCodec.createDecoderByType(MIME_TYPE);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //初始化MediaFormat
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE,
                width, height);
        //配置MediaFormat以及需要顯示的surface
        mCodec.configure(mediaFormat, holder.getSurface(), null, 0);
        //開始解碼
        mCodec.start();
        isFirst = false;
    }

    int mCount = 0;


    public boolean onFrame(byte[] buf, int offset, int length) {
        // 獲取輸入buffer index
        ByteBuffer[] inputBuffers = mCodec.getInputBuffers();
        //-1表示一直等待;0表示不等待;其他大於0的參數表示等待毫秒數
        int inputBufferIndex = mCodec.dequeueInputBuffer(-1);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            //清空buffer
            inputBuffer.clear();
            //put需要解碼的數據
            inputBuffer.put(buf, offset, length);
            //解碼
            mCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * TIME_INTERNAL, 0);
            mCount++;

        } else {
            return false;
        }
        // 獲取輸出buffer index
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, 100);
        //循環解碼,直到數據全部解碼完成
        while (outputBufferIndex >= 0) {
            //logger.d("outputBufferIndex = " + outputBufferIndex);
            //true : 將解碼的數據顯示到surface上
            mCodec.releaseOutputBuffer(outputBufferIndex, true);
            outputBufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, 0);
        }
        if (outputBufferIndex < 0) {
            //logger.e("outputBufferIndex = " + outputBufferIndex);
        }
        return true;
    }

    /**
    *停止解碼,釋放解碼器
    */
    public void stopCodec() {
    
        try {
            mCodec.stop();
            mCodec.release();
            mCodec = null;
            isFirst = true;
        } catch (Exception e) {
            e.printStackTrace();
            mCodec = null;
        }
    }
}

讀取文件線程

這部分代碼的主要功能是利用線程去讀取指定的h264文件,通過判斷I幀或者P幀的幀頭來讀取每一幀的數據送入解碼器進行解碼,並根據幀率進行休眠。

/**
 * Created by ZhangHao on 2017/5/5.
 * 讀取H264文件送入解碼器解碼線程
 */

public class MediaCodecThread extends Thread {
    //自定義的log打印,可以無視
    Logger logger = Logger.getLogger();
    //解碼器
    private MediaCodecUtil util;
    //文件路徑
    private String path;
    //文件讀取完成標識
    private boolean isFinish = false;
    //這個值用於找到第一個幀頭後,繼續尋找第二個幀頭,如果解碼失敗可以嘗試縮小這個值
    private int FRAME_MIN_LEN = 1024;
    //一般H264幀大小不超過200k,如果解碼失敗可以嘗試增大這個值
    private static int FRAME_MAX_LEN = 300 * 1024;
    //根據幀率獲取的解碼每幀需要休眠的時間,根據實際幀率進行操作
    private int PRE_FRAME_TIME = 1000 / 25;

    /**
     * 初始化解碼器
     *
     * @param util 解碼Util
     * @param path 文件路徑
     */
    public MediaCodecThread(MediaCodecUtil util, String path) {
        this.util = util;
        this.path = path;
    }

     /**
     * 尋找指定buffer中h264頭的開始位置
     *
     * @param data   數據
     * @param offset 偏移量
     * @param max    需要檢測的最大值
     * @return h264頭的開始位置 ,-1表示未發現
     */
    private int findHead(byte[] data, int offset, int max) {
        int i;
        for (i = offset; i <= max; i++) {
            //發現幀頭
            if (isHead(data, i))
                break;
        }
        //檢測到最大值,未發現幀頭
        if (i == max) {
            i = -1;
        }
        return i;
    }

    /**
     * 判斷是否是I幀/P幀頭:
     * 00 00 00 01 65    (I幀)
     * 00 00 00 01 61 / 41   (P幀)
     *
     * @param data
     * @param offset
     * @return 是否是幀頭
     */
    private boolean isHead(byte[] data, int offset) {
        boolean result = false;
        // 00 00 00 01 x
        if (data[offset] == 0x00 && data[offset + 1] == 0x00
                && data[offset + 2] == 0x00 && data[3] == 0x01 && isVideoFrameHeadType(data[offset + 4])) {
            result = true;
        }
        // 00 00 01 x
        if (data[offset] == 0x00 && data[offset + 1] == 0x00
                && data[offset + 2] == 0x01 && isVideoFrameHeadType(data[offset + 3])) {
            result = true;
        }
        return result;
    }

    /**
     * I幀或者P幀
     */
    private boolean isVideoFrameHeadType(byte head) {
        return head == (byte) 0x65 || head == (byte) 0x61 || head == (byte) 0x41;
    }

    @Override
    public void run() {
        super.run();
        File file = new File(path);
        //判斷文件是否存在
        if (file.exists()) {
            try {
                FileInputStream fis = new FileInputStream(file);
                //保存完整數據幀
                byte[] frame = new byte[FRAME_MAX_LEN];
                //當前幀長度
                int frameLen = 0;
                //每次從文件讀取的數據
                byte[] readData = new byte[10 * 1024];
                //開始時間
                long startTime = System.currentTimeMillis();
                //循環讀取數據
                while (!isFinish) {
                    if (fis.available() > 0) {
                        int readLen = fis.read(readData);
                        //當前長度小於最大值
                        if (frameLen + readLen < FRAME_MAX_LEN) {
                            //將readData拷貝到frame
                            System.arraycopy(readData, 0, frame, frameLen, readLen);
                            //修改frameLen
                            frameLen += readLen;
                            //尋找第一個幀頭
                            int headFirstIndex = findHead(frame, 0, frameLen);
                            while (headFirstIndex >= 0 && isHead(frame, headFirstIndex)) {
                                //尋找第二個幀頭
                                int headSecondIndex = findHead(frame, headFirstIndex + FRAME_MIN_LEN, frameLen);
                                //如果第二個幀頭存在,則兩個幀頭之間的就是一幀完整的數據
                                if (headSecondIndex > 0 && isHead(frame, headSecondIndex)) {
                                    logger.e("headSecondIndex:" + headSecondIndex);
                                    //視頻解碼
                                    onFrame(frame, headFirstIndex, headSecondIndex - headFirstIndex);
                                    //截取headSecondIndex之後到frame的有效數據,並放到frame最前面
                                    byte[] temp = Arrays.copyOfRange(frame, headSecondIndex, frameLen);
                                    System.arraycopy(temp, 0, frame, 0, temp.length);
                                    //修改frameLen的值
                                    frameLen = temp.length;
                                    //線程休眠
                                    sleepThread(startTime, System.currentTimeMillis());
                                    //重置開始時間
                                    startTime = System.currentTimeMillis();
                                    //繼續尋找數據幀
                                    headFirstIndex = findHead(frame, 0, frameLen);
                                } else {
                                    //找不到第二個幀頭
                                    headFirstIndex = -1;
                                }
                            }
                        } else {
                            //如果長度超過最大值,frameLen置0
                            frameLen = 0;
                        }
                    } else {
                        //文件讀取結束
                        isFinish = true;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            logger.e("File not found");
        }
    }

    //視頻解碼
    private void onFrame(byte[] frame, int offset, int length) {
        if (util != null) {
            try {
                util.onFrame(frame, offset, length);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            logger.e("mediaCodecUtil is NULL");
        }
    }

    //修眠
    private void sleepThread(long startTime, long endTime) {
        //根據讀文件和解碼耗時,計算需要休眠的時間
        long time = PRE_FRAME_TIME - (endTime - startTime);
        if (time > 0) {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //手動終止讀取文件,結束線程
    public void stopThread() {
        isFinish = true;
    }
}

在 MainActivity中調用,先將目標文件放到 sdcard /downlaod 文件夾,在 AndroidManifest.xml 增加 權限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

並在 運行環境中 允許 操作 sd卡,才能順利 打開文件

package com.example.mcodec;

import androidx.appcompat.app.AppCompatActivity;


import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.TextView;


public class MainActivity extends AppCompatActivity {

    private SurfaceView testSurfaceView;
    private SurfaceHolder holder;
    private TextView txView;
    private String test;
    //解碼器
    private MediaCodecUtil codecUtil;
    //讀取文件解碼線程
    private MediaCodecThread thread;
    //文件路徑
    private String path =Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)+"/test.h264"; //"file:///android_asset/test.h264";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testSurfaceView=findViewById(R.id.surfaceView);
//        txView=findViewById(R.id.textView2);
//        //txView.setText(getFilesDir().getPath());
//        txView.setText(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)+"/test.h264");
//       // Log.i("MainActivity",getAssets().getLocales()[0]);
        initSurface();
    }

    private void initSurface() {
        holder = testSurfaceView.getHolder();
        holder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

            }
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                if (codecUtil == null) {
                    codecUtil = new MediaCodecUtil(holder);
                    codecUtil.startCodec();
                }
                if (thread == null) {
                    //解碼線程第一次初始化
                    thread = new MediaCodecThread(codecUtil, path);
                    thread.start();
                }
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                if (codecUtil != null) {
                    codecUtil.stopCodec();
                    codecUtil = null;
                }
                if (thread != null && thread.isAlive()) {
                    thread.stopThread();
                    thread = null;
                }
            }


        });
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章