Android MediaCodec硬解码H264文件

来源:互联网 发布:淘宝灯笼是什么意思 编辑:程序博客网 时间:2024/05/16 10:07

转载请注明出处:http://blog.csdn.net/a512337862/article/details/72629779

最近开始接触Android MediaCodec,经过学习之后总算是有点简单的收获,所以在这里总结一下,希望能帮到一些有需要的人。今天主要是关于利用MediaCodec解码H264文件之后利用SurfaceView进行显示:

封装解码器

这部分代码主要功能是对MediaCodec进行封装,实现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;    }}

Activity

这里的代码就是在activity中对一个h264文件进行解码播放,布局文件只有一个surfaceView和一个Button,我就不贴出来了,下面的是activity的代码:

public class H264FileDecodeActivity extends AppCompatActivity {    @Bind(R.id.test_surface_view)    SurfaceView testSurfaceView;    private SurfaceHolder holder;    //解码器    private MediaCodecUtil codecUtil;    //读取文件解码线程    private MediaCodecThread thread;    //文件路径    private String path = Environment.getExternalStorageDirectory().toString() + "/SONA/test.h264";    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_h264_file_decodec);        ButterKnife.bind(this);        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);                }            }            @Override            public void surfaceDestroyed(SurfaceHolder holder) {                if (codecUtil != null) {                    codecUtil.stopCodec();                    codecUtil = null;                }                if (thread != null && thread.isAlive()) {                    thread.stopThread();                    thread = null;                }            }        });    }    public void onClick(View view) {        switch (view.getId()) {            case R.id.play_h264_file:                if (thread != null) {                    thread.start();                }                break;        }    }}

结语

1.因为文字功底太差,所以主要以贴代码为主,有什么大家疑问可以给我留言。接下来有空的话,我会简单介绍如何利用MediaCodec将AAC解码成PCM并利用AudioTrack进行播放。
2.2.H264,AAC解码Demo下载地址:http://download.csdn.net/detail/a512337862/9882200 ,附带aac以及H264文件以及源码。

原创粉丝点击