使用MediaCodec硬解码h.265视频及音频进行播放

5,230 阅读4分钟

h.265这个视频是很多播放器不支持的,就算是bilibili开源的ijkplayer也不能直接播放,需要自己去重新编译 才可以支持。 这里通过这个demo来演示一下如何硬解码视频,播放h.265视频,其实编码的视频同样道理。

视频的播放主要在surfaceView中显示,而解码过程则在音频解码线程视频解码线程两个线程中分别执行。

视频解码

主要是用到了一个MediaCodec这个类来进行解码。

设置数据源

MediaExtractor mediaExtractor = new MediaExtractor();
try {
    mediaExtractor.setDataSource(path); // 设置数据源
} catch (IOException e1) {
    e1.printStackTrace();
}

根据视频的编码信息来初始化MediaCodec:

视频的mineType是video类型。

String mimeType = null;
for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { // 信道总数
    MediaFormat format = mediaExtractor.getTrackFormat(i); // 音频文件信息
    mimeType = format.getString(MediaFormat.KEY_MIME);
    if (mimeType.startsWith("video/")) { // 视频信道
        mediaExtractor.selectTrack(i); // 切换到视频信道
        try {
            mediaCodec = MediaCodec.createDecoderByType(mimeType); // 创建解码器,提供数据输出
        } catch (IOException e) {
            e.printStackTrace();
        }
        mediaCodec.configure(format, surface, null, 0);
        break;
    }
}
mediaCodec.start(); // 启动MediaCodec ,等待传入数据

获取缓存器

// 输入
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers(); // 用来存放目标文件的数据
// 输出
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers(); // 解码后的数据
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); // 用于描述解码得到的byte[]数据的相关信息

开始解码

while (!Thread.interrupted()) {

    if (!bIsEos) {
        int inIndex = mediaCodec.dequeueInputBuffer(0);
        if (inIndex >= 0) {
            ByteBuffer buffer = inputBuffers[inIndex];
            int nSampleSize = mediaExtractor.readSampleData(buffer, 0); // 读取一帧数据至buffer中
            if (nSampleSize < 0) {
                Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
                mediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                bIsEos = true;
            } else {
                // 填数据
                mediaCodec.queueInputBuffer(inIndex, 0, nSampleSize, mediaExtractor.getSampleTime(), 0); // 通知MediaDecode解码刚刚传入的数据
                mediaExtractor.advance(); // 继续下一取样
            }
        }
    }

    int outIndex = mediaCodec.dequeueOutputBuffer(info, 0);
    switch (outIndex) {
        case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
            Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
            outputBuffers = mediaCodec.getOutputBuffers();
            break;
        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
            Log.d(TAG, "New format " + mediaCodec.getOutputFormat());
            break;
        case MediaCodec.INFO_TRY_AGAIN_LATER:
            Log.d(TAG, "dequeueOutputBuffer timed out!");
            break;
        default:
            ByteBuffer buffer = outputBuffers[outIndex];
            Log.v(TAG, "We can't use this buffer but render it due to the API limit, " + buffer);

            mediaCodec.releaseOutputBuffer(outIndex, true);
            break;
    }

    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
        Log.d("DecodeActivity", "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
        break;
    }
}

解码完成后释放资源

mediaCodec.stop();
mediaCodec.release();
mediaExtractor.release();

这样视频的解码就已经完成了,此时surfaceView已经可以播放视频了,接下来是音频解码。

音频解码

音频解码的过程和上面大同小异,主要区别在于,视频是用surfaceView播放显示的,而音频我们需要使用AudioTrack来播放。

创建一个AudioPlayer类用于播放音频

public class AudioPlayer {
    private int mFrequency;// 采样率
    private int mChannel;// 声道
    private int mSampBit;// 采样精度
    private AudioTrack mAudioTrack;

    public AudioPlayer(int frequency, int channel, int sampbit) {
        this.mFrequency = frequency;
        this.mChannel = channel;
        this.mSampBit = sampbit;
    }

    /**
     * 初始化
     */
    public void init() {
        if (mAudioTrack != null) {
            release();
        }
        // 获得构建对象的最小缓冲区大小
        int minBufSize = AudioTrack.getMinBufferSize(mFrequency, mChannel, mSampBit);
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                mFrequency, mChannel, mSampBit, minBufSize, AudioTrack.MODE_STREAM);
        mAudioTrack.play();
    }

    /**
     * 释放资源
     */
    private void release() {
        if (mAudioTrack != null) {
            mAudioTrack.stop();
            mAudioTrack.release();
        }
    }

    /**
     * 将解码后的pcm数据写入audioTrack播放
     *
     * @param data   数据
     * @param offset 偏移
     * @param length 需要播放的长度
     */
    public void play(byte[] data, int offset, int length) {
        if (data == null || data.length == 0) {
            return;
        }
        try {
            mAudioTrack.write(data, offset, length);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

初始化音频解码器:

音频的mineType是audio类型,我们根据这个来去音频信息即可。

String mimeType;
for (int i = 0; i < mediaExtractor.getTrackCount(); i++) { // 信道总数
    MediaFormat format = mediaExtractor.getTrackFormat(i); // 音频文件信息
    mimeType = format.getString(MediaFormat.KEY_MIME);
    if (mimeType.startsWith("audio/")) { // 音频信道
        mediaExtractor.selectTrack(i); // 切换到 音频信道
        try {
            mediaCodec = MediaCodec.createDecoderByType(mimeType); // 创建解码器,提供数据输出
        } catch (IOException e) {
            e.printStackTrace();
        }
        mediaCodec.configure(format, null, null, 0);
        mPlayer = new AudioPlayer(format.getInteger(MediaFormat.KEY_SAMPLE_RATE), AudioFormat
                .CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
        mPlayer.init();
        break;
    }
}
if (mediaCodec == null) {
    Log.e(TAG, "Can't find video info!");
    return;
}

mediaCodec.start(); // 启动MediaCodec ,等待传入数据

音频解码:

音频解码过程与视频解码大同小异,只需要额外调用一下我们创建的AudioPlayer来播放音频即可。

while (!Thread.interrupted()) {

    if (!bIsEos) {
        int inIndex = mediaCodec.dequeueInputBuffer(0);
        if (inIndex >= 0) {
            ByteBuffer buffer = inputBuffers[inIndex];
            int nSampleSize = mediaExtractor.readSampleData(buffer, 0); // 读取一帧数据至buffer中
            if (nSampleSize < 0) {
                Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
                mediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                bIsEos = true;
            } else {
                // 填数据
                mediaCodec.queueInputBuffer(inIndex, 0, nSampleSize, mediaExtractor.getSampleTime(), 0); // 通知MediaDecode解码刚刚传入的数据
                mediaExtractor.advance(); // 继续下一取样
            }
        }
    }

    int outIndex = mediaCodec.dequeueOutputBuffer(info, 0);
    switch (outIndex) {
        case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
            Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
            outputBuffers = mediaCodec.getOutputBuffers();
            break;
        case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
            Log.d(TAG, "New format " + mediaCodec.getOutputFormat());
            break;
        case MediaCodec.INFO_TRY_AGAIN_LATER:
            Log.d(TAG, "dequeueOutputBuffer timed out!");
            break;
        default:
            ByteBuffer buffer = outputBuffers[outIndex];
            Log.v(TAG, "We can't use this buffer but render it due to the API limit, " + buffer);

            while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
                try {
                    sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
            //用来保存解码后的数据
            byte[] outData = new byte[info.size];
            buffer.get(outData);
            //清空缓存
            buffer.clear();
            //播放解码后的数据
            mPlayer.play(outData, 0, info.size);
            mediaCodec.releaseOutputBuffer(outIndex, true);
            break;
    }

    // All decoded frames have been rendered, we can stop playing
    // now
    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
        Log.d("DecodeActivity", "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
        break;
    }
}

效果展示

视频编码信息,为h.265:

播放效果(带声音):

视频播放

获取MediaCodec支持解码的编码格式:

HashMap<String, MediaCodecInfo.CodecCapabilities> mEncoderInfos = new HashMap<>();
for(int i = MediaCodecList.getCodecCount() - 1; i >= 0; i--){
    MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
    if(codecInfo.isEncoder()){
        for(String t : codecInfo.getSupportedTypes()){
            try{
                mEncoderInfos.put(t, codecInfo.getCapabilitiesForType(t));
            } catch(IllegalArgumentException e){
                e.printStackTrace();
            }
        }
    }
}

完整demo地址

使用的时候将assets下的h265.mp4复制到sd卡即可

gitHub地址:github.com/JavaNoober/…