美文网首页Android 开发
Android MediaCodec硬解码H264文件

Android MediaCodec硬解码H264文件

作者: BlackMagician | 来源:发表于2017-05-08 18:36 被阅读1417次

    最近开始接触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;
            }
        }
    }
    

    结语

    因为文字功底太差,所以主要以贴代码为主,有什么大家疑问可以给我留言。接下来有空的话,我会简单介绍如何利用MediaCodec将AAC解码成PCM并利用AudioTrack进行播放。

    相关文章

      网友评论

      • 50c8dc99c5fa:好像没什么用。
      • 048f398ea7b5:既然都已经把主要的代码贴出来了,就不能把工程链接也发一下么,卤煮

      本文标题:Android MediaCodec硬解码H264文件

      本文链接:https://www.haomeiwen.com/subject/kgsmtxtx.html