1. 程式人生 > >使用MediaCodec硬解碼h.265視訊及音訊進行播放

使用MediaCodec硬解碼h.265視訊及音訊進行播放

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地址,歡迎star:https://github.com/JavaNoober/MedioDecode