Android視頻編解碼

簡介

在這裏插入圖片描述
  從廣義上講,編解碼器就是處理輸入數據來產生輸出數據。MediaCode採用異步方式處理數據,並且使用了一組輸入輸出緩存(input and output buffers)。簡單來講,你請求或接收到一個空的輸入緩存(input buffer),向其中填充滿數據並將它傳遞給編解碼器處理。編解碼器處理完這些數據並將處理結果輸出至一個空的輸出緩存(output buffer)中。最終,你請求或接收到一個填充了結果數據的輸出緩存(output buffer),使用完其中的數據,並將其釋放給編解碼器再次使用。

狀態(States)

在編解碼器的生命週期內有三種理論狀態:停止態-Stopped、執行態-Executing、釋放態-Released,停止狀態(Stopped)包括了三種子狀態:未初始化(Uninitialized)、配置(Configured)、錯誤(Error)。執行狀態(Executing)在概念上會經歷三種子狀態:刷新(Flushed)、運行(Running)、流結束(End-of-Stream)。 在這裏插入圖片描述

  • 當你使用任意一種工廠方法(factory methods)創建了一個編解碼器,此時編解碼器處於未初始化狀態(Uninitialized)。首先,你需要使用configure(…)方法對編解碼器進行配置,這將使編解碼器轉爲配置狀態(Configured)。然後調用start()方法使其轉入執行狀態(Executing)。在這種狀態下你可以通過上述的緩存隊列操作處理數據。
  • 執行狀態(Executing)包含三個子狀態: 刷新(Flushed)、運行( Running) 以及流結束(End-of-Stream)。在調用start()方法後編解碼器立即進入刷新子狀態(Flushed),此時編解碼器會擁有所有的緩存。一旦第一個輸入緩存(input buffer)被移出隊列,編解碼器就轉入運行子狀態(Running),編解碼器的大部分生命週期會在此狀態下度過。當你將一個帶有end-of-stream 標記的輸入緩存入隊列時,編解碼器將轉入流結束子狀態(End-of-Stream)。在這種狀態下,編解碼器不再接收新的輸入緩存,但它仍然產生輸出緩存(output buffers)直到end-of- stream標記到達輸出端。你可以在執行狀態(Executing)下的任何時候通過調用flush()方法使編解碼器重新返回到刷新子狀態(Flushed)。
  • 通過調用stop()方法使編解碼器返回到未初始化狀態(Uninitialized),此時這個編解碼器可以再次重新配置 。當你使用完編解碼器後,你必須調用release()方法釋放其資源。
  • 在極少情況下編解碼器會遇到錯誤並進入錯誤狀態(Error)。這個錯誤可能是在隊列操作時返回一個錯誤的值或者有時候產生了一個異常導致的。通過調用 reset()方法使編解碼器再次可用。你可以在任何狀態調用reset()方法使編解碼器返回到未初始化狀態(Uninitialized)。否則,調用 release()方法進入最終的Released狀態。

一、編碼

初始化編碼器

	public void prepare(int width, int height) throws IOException {
		// MIME_TYPE:"video/avc" -> H264  "video/hevc" -> H265
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);

        mWidth = width;
        mHeight = height;

        // Set some properties.  Failing to specify some of these can cause the MediaCodec
        // configure() call to throw an unhelpful exception.
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
        format.setInteger(MediaFormat.KEY_BIT_RATE, VideoConfig.BIT_RATE);
        // FPS 每秒傳輸幀數(Frames Per Second)
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
        // I-frame 關鍵幀時間間隔,單位min
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        Log.d(TAG, "format: " + format);
        
        // Create a MediaCodec encoder, and configure it with our format.  Get a Surface
        // we can use for input and wrap it with a class that handles the EGL work.
        mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
        mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mMediaFormat = mEncoder.getOutputFormat();
        mEncoder.start();
    }

向InputBuffer輸入編碼數據

如果從Camera拿到的數據爲NV21/NV12格式,可以先通過 YUV庫 將NV21/NV12轉碼爲I420格式,再將數據送入編碼器編碼

	/**
	 * 向編碼器InputBuffer中填入數據
	 *
	 * @param data       NV21數據
	 * @param timeSptamp 時間戳 ms
	 */
	private void putDataToInputBuffer(byte[] data, long timeSptamp) {
        int index = mEncoder.dequeueInputBuffer(-1);
        if (index >= 0) {
            ByteBuffer buffer = mEncoder.getInputBuffer(index);
            if (buffer == null) {
                Log.d(TAG, "InputBuffer is null point");
                return;
            }
            if (yuv == null) {
            	// YUV數據存儲空間大小爲 Y分量->width * height U、V分量->width * height / 4
                yuv = new byte[mWidth * mHeight * 3 / 2];
            }
            // NV21格式數據轉爲I420P
            nv21ToYuv420p(data, timeSptamp);
            buffer.clear();
            buffer.put(yuv);

            mEncoder.queueInputBuffer(index, 0, data.length, timeSptamp * 1000, 0);

        }
        drainEncoder(false);
    }

說明:視頻添加文字/圖片水印,可以在將YUV數據送入編碼器前,將文字轉爲Bitmap,通過YUV庫將ARGB轉碼爲I420P,再使用YUV圖片合成技術合成,這樣編碼後的H264/H265視頻碼流就添加上了水印。

處理OutputBuffer

	/**
     * 讀取編碼後的H264/H265數據
     *
     * @param endOfStream 標識是否結束
     */
    public void drainEncoder(boolean endOfStream) {
        final int TIMEOUT_USEC = 10000;

        if (endOfStream) {
            Log.d(TAG, "sending EOS to encoder");
            return;
        }
        while (true) {
            int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
                if (!endOfStream) {
                    break;      // out of while
                } else {
                    Log.d(TAG, "no output available, spinning to await EOS");
                }
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // should happen before receiving buffers, and should only happen once
                mMediaFormat = mEncoder.getOutputFormat();
                Log.d(TAG, "encoder output format changed: " + mMediaFormat);
            } else if (encoderStatus < 0) {
                Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
                        encoderStatus);
                // let's ignore it
            } else {
                ByteBuffer encodedData = mEncoder.getOutputBuffer(encoderStatus);
                if (encodedData == null) {
                    Log.w(TAG, "encoderOutputBuffer " + encoderStatus +
                            " was null");
                    break;
                }

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // The codec config data was pulled out and fed to the muxer when we got
                    // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                    Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    encodedData.position(mBufferInfo.offset);
                    encodedData.limit(mBufferInfo.offset + mBufferInfo.size);

                    // 取出編碼好的H264數據
                    byte[] data = new byte[mBufferInfo.size];
                    encodedData.get(data);

                    // todo 將編碼好的H264/H265數據存儲到緩衝區或者傳遞給MediaMuxer生成視頻文件(MP4)
					// mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME 表示該幀數據爲關鍵幀(I幀)
                    Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
                            mBufferInfo.presentationTimeUs);
                }

				// 釋放輸出緩衝區
                mEncoder.releaseOutputBuffer(encoderStatus, false);

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (!endOfStream) {
                        Log.d(TAG, "reached end of stream unexpectedly");
                    } else {
                        Log.d(TAG, "end of stream reached");
                    }
                    break;      // out of while
                }
            }
        }
    }

二、解碼

基礎知識

1、Codec-specific數據

有些格式,特別是ACC音頻和MPEG4、H.264和H.265視頻格式要求實際數據以若干個包含配置數據或編解碼器指定數據的緩存爲前綴。當處理這種壓縮格式的數據時,這些數據必須在調用start()方法後且在處理任何幀數據之前提交給編解碼器。這些數據必須在調用queueInputBuffer方法時使用BUFFER_FLAG_CODEC_CONFIG進行標記。

Codec-specific數據也可以被包含在傳遞給configure方法的格式信息(MediaFormat)中,在ByteBuffer條目中以"csd-0", "csd-1"等key標記。這些keys一直包含在通過MediaExtractor獲得的Audio Track or Video Track的MediaFormat中。一旦調用start()方法,MediaFormat中的Codec-specific數據會自動提交給編解碼器;你不能顯示的提交這些數據。如果MediaFormat中不包含編解碼器指定的數據,你可以根據格式要求,按照正確的順序使用指定數目的緩存來提交codec-specific數據。在H264 AVC編碼格式下,你也可以連接所有的codec-specific數據並作爲一個單獨的codec-config buffer提交。

Android 使用下列的codec-specific data buffers。對於適當的MediaMuxer軌道配置,這些也要在軌道格式中進行設置。每一個參數集以及被標記爲(*)的codec-specific-data段必須以"\x00\x00\x00\x01"字符開頭。
在這裏插入圖片描述
注意:當編解碼器被立即刷新或start之後不久刷新,並且在任何輸出buffer或輸出格式變化被返回前需要特別地小心,因爲編解碼器的codec specific data可能會在flush過程中丟失。爲保證編解碼器的正常運行,你必須在刷新後使用標記爲BUFFER_FLAG_CODEC_CONFIG的buffers再次提交這些數據。

2、流域界與關鍵幀(Stream Boundary and Key Frames)

調用start()或flush()方法後,輸入數據在合適的流邊界開始是非常重要的:其第一幀必須是關鍵幀(key-frame)。一個關鍵幀能夠獨立地完全解碼(對於大多數編解碼器它意味着I-frame),關鍵幀之後顯示的幀不會引用關鍵幀之前的幀。

下面的表格針對不同的視頻格式總結了合適的關鍵幀:
在這裏插入圖片描述

核心代碼

初始化解碼器
	public void prepare(int width, int height, int fps, byte[] sps, byte[] pps) throws IOException {
        String mimeType = "video/avc";
        MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);

        mWidth = width;
        mHeight = height;

		// 參見Codec-specific數據說明,H264數據格式需要 csd-0(sps)、csd-1(pps);
		// H265數據格式需要 csd-0(vps+sps+pps)
        if (sps != null) {
            format.setByteBuffer("csd-0", ByteBuffer.wrap(sps));
        }
        if (pps != null) {
            format.setByteBuffer("csd-1", ByteBuffer.wrap(pps));
        }

        format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
        format.setInteger(MediaFormat.KEY_PUSH_BLANK_BUFFERS_ON_STOP, 1);
        
        Log.i(TAG, String.format("config codec:%s", format));
		// 創建解碼器
        mDecoder = MediaCodec.createDecoderByType(mimeType);
        // 配置解碼器 format
        mDecoder.configure(format, null, null, 0);
        mDecoder.start();
    }

注意:在解碼H264/H265數據時,傳遞碼流數據前一定要先配置好csd-*,參見Codec-specific說明。

向InputBuffer輸入解碼數據
    /**
     * 向解碼器InputBuffer中填入數據
     *
     * @param data       H264/H265數據
     * @param timeSptamp 時間戳 us
     */
    private void putDataToInputBuffer(byte[] data, long timeSptamp) {
        int index = mDecoder.dequeueInputBuffer(-1);
        if (index >= 0) {
            ByteBuffer buffer = mDecoder.getInputBuffer(index);
            if (buffer == null) {
                LogUtils.d(TAG, "InputBuffer is null point");
                return;
            }
            buffer.clear();
            buffer.put(data);

            Log.d(TAG, "queueInputBuffer data length: " + data.length + "  timeSptamp: " + timeSptamp);
            mDecoder.queueInputBuffer(index, 0, data.length, timeSptamp, 0);

        }
        drainDecoder(false, timeSptamp);
    }

注意:傳遞給解碼器的第一幀數據必須是關鍵幀(I-幀),參見流域界與關鍵幀說明。

處理OutputBuffer
    /**
     * 讀取解碼後的H264/H265數據
     *
     * @param endOfStream 標識是否結束
     * @param timeSptamp  當前解碼的數據的時間戳
     */
    private void drainDecoder(boolean endOfStream, long timeSptamp) {
        final int TIMEOUT_USEC = 10000;

        if (endOfStream) {
            Log.d(TAG, "sending EOS to encoder");
            return;
        }
        while (true) {
            int decoderStatus = mDecoder.dequeueOutputBuffer(mBufferInfo, 0);
            if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet  輸出爲空
                if (!endOfStream) {
                    break;      // out of while
                } else {
                    Log.d(TAG, "no output available, spinning to await EOS");
                }
            } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // should happen before receiving buffers, and should only happen once
                mMediaFormat = mDecoder.getOutputFormat();
                Log.d(TAG, "encoder output format changed: " + mMediaFormat);
            } else if (decoderStatus < 0) {
                Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
                        decoderStatus);
                // let's ignore it
            } else {

                ByteBuffer decodedData = mDecoder.getOutputBuffer(decoderStatus);
                if (decodedData == null) {
                    Log.w(TAG, "decoderOutputBuffer " + decoderStatus +
                            " was null");
                    break;
                }

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // The codec config data was pulled out and fed to the muxer when we got
                    // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                    Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    decodedData.position(mBufferInfo.offset);
                    decodedData.limit(mBufferInfo.offset + mBufferInfo.size);

                    // 取出解碼好的NV12數據
                    byte[] data = new byte[mBufferInfo.size];
                    decodedData.get(data);

					// todo 可以將解碼後的NV12數據轉碼爲ARGB8888,保存爲jpg圖片
                    Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
                            mBufferInfo.presentationTimeUs);
                }

                mDecoder.releaseOutputBuffer(decoderStatus, false);
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (!endOfStream) {
                        Log.d(TAG, "reached end of stream unexpectedly");
                    } else {
                        Log.d(TAG, "end of stream reached");
                    }
                    break;      // out of while
                }
            }
        }
    }

說明:解碼後的NV12數據可以通過YUV庫轉碼爲ARGB8888格式,再將ARGB8888轉爲Bitmap對象,從而保存爲jpeg格式的圖片文件。

// 將ARGB8888原始數據轉爲Bitmap對象
Bitmap bitmap= Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(argb));

參考文獻

https://developer.android.google.cn/reference/android/media/MediaCodec
https://github.com/google/grafika
Android Camera預覽時輸出的幀率控制
https://chromium.googlesource.com/libyuv/libyuv/
使用libyuv對YUV數據進行縮放,旋轉,鏡像,裁剪等操作
YUV圖像的水印的添加
EasyPlayer一款精煉、高效、穩定的流媒體播放器
H264(NAL簡介與I幀判斷)

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