Android 用MediaCodec實現視頻硬解碼


本 文向你講述如何用android標准的API (MediaCodec)實現視頻的硬件編解碼。例程將從攝像頭采集視頻開始,然后進行H264編碼,再解碼,然后顯示。

1、從攝像頭采集視頻

      可以通過攝像頭Preview的回調,來獲取視頻數據。

      首先創建攝像頭,並設置參數:

 

[java]  view plain copy
  1.                      cam = Camera.open();  
  2. cam.setPreviewDisplay(holder);                    
  3. Camera.Parameters parameters = cam.getParameters();  
  4. parameters.setFlashMode("off"); // 無閃光燈  
  5. parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);  
  6. parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);  
  7. parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);   
  8. parameters.setPreviewFormat(ImageFormat.YV12);       
  9. parameters.setPictureSize(camWidth, camHeight);  
  10. parameters.setPreviewSize(camWidth, camHeight);  
  11.     //這兩個屬性 如果這兩個屬性設置的和真實手機的不一樣時,就會報錯  
  12. cam.setParameters(parameters);            

寬度和高度必須是攝像頭支持的尺寸,否則會報錯。要獲得所有支持的尺寸,可用getSupportedPreviewSizes,這里不再累述。據說所有的參數必須設全,漏掉一個就可能報錯,不過只是據說,我只設了幾個屬性也沒出錯。    然后就開始Preview了:

[java]  view plain copy
  1. buf = new byte[camWidth * camHeight * 3 / 2];  
  2. cam.addCallbackBuffer(buf);  
  3. cam.setPreviewCallbackWithBuffer(this);           
  4. cam.startPreview();   

  setPreviewCallbackWithBuffer是很有必要的,不然每次回調系統都重新分配緩沖區,效率會很低。

    在onPreviewFrame中就可以獲得原始的圖片了(當然,this 肯定要 implements PreviewCallback了)。這里我們是把它傳給編碼器:

[java]  view plain copy
  1. public void onPreviewFrame(byte[] data, Camera camera) {  
  2.     if (frameListener != null) {  
  3.         frameListener.onFrame(data, 0, data.length, 0);  
  4.     }  
  5.     cam.addCallbackBuffer(buf);  
  6. }  

2、編碼

    首先要初始化編碼器:

[java]  view plain copy
  1.       mediaCodec = MediaCodec.createEncoderByType("Video/AVC");  
  2. MediaFormat mediaFormat = MediaFormat.createVideoFormat(type, width, height);  
  3. mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);  
  4. mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);  
  5. mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);  
  6. mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);  
  7. mediaCodec.configure(mediaFormat, nullnull, MediaCodec.CONFIGURE_FLAG_ENCODE);  
  8. mediaCodec.start();  


    然后就是給他喂數據了,這里的數據是來自攝像頭的:

[java]  view plain copy
  1. public void onFrame(byte[] buf, int offset, int length, int flag) {  
  2.    ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();  
  3.    ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();  
  4.    int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);  
  5.    if (inputBufferIndex >= 0)  
  6.        ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];  
  7.        inputBuffer.clear();  
  8.        inputBuffer.put(buf, offset, length);  
  9.        mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, 00);  
  10.    }  
  11.    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();  
  12.    int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);  
  13.    while (outputBufferIndex >= 0) {  
  14.        ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];  
  15.        if (frameListener != null)  
  16.            frameListener.onFrame(outputBuffer, 0, length, flag);  
  17.        mediaCodec.releaseOutputBuffer(outputBufferIndex, false);  
  18.        outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);  
  19.    }   

先把來自攝像頭的數據喂給它,然后從它里面取壓縮好的數據喂給解碼器。

3、解碼和顯示

     首先初始化解碼器:

[java]  view plain copy
  1. mediaCodec = MediaCodec.createDecoderByType("Video/AVC");  
  2. MediaFormat mediaFormat = MediaFormat.createVideoFormat(mime, width, height);  
  3. mediaCodec.configure(mediaFormat, surface, null0);  
  4. mediaCodec.start();  

             這里通過給解碼器一個surface,解碼器就能直接顯示畫面。

     然后就是處理數據了:

[java]  view plain copy
  1. public void onFrame(byte[] buf, int offset, int length, int flag) {  
  2.         ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();  
  3.             int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);  
  4.         if (inputBufferIndex >= 0) {  
  5.             ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];  
  6.             inputBuffer.clear();  
  7.             inputBuffer.put(buf, offset, length);  
  8.             mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 1000000 / FRAME_RATE, 0);  
  9.                    mCount++;  
  10.         }  
  11.   
  12.        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();  
  13.        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);  
  14.        while (outputBufferIndex >= 0) {  
  15.            mediaCodec.releaseOutputBuffer(outputBufferIndex, true);  
  16.            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);  
  17.        }  
  18. }  

        queueInputBuffer第三個參數是時間戳,其實怎么寫都無所謂,只要是按時間線性增加的就可以,這里就隨便弄一個了。后面一段的代碼就是把緩 沖區給釋放掉,因為我們直接讓解碼器顯示,就不需要解碼出來的數據了,但是必須要這么釋放一下,否則解碼器始終給你留着,內存就該不夠用了。

 

好了,到現在,基本上就可以了。如果你運氣夠好,現在就能看到視頻了,比如在我的三星手機上這樣就可以了。但是,我試過幾個其他平台,多數都不可以,總是有各種各樣的問題,如果要開發一個不依賴平台的應用,還有很多的問題要解決。說說我遇到的一些情況:

 

1、視頻尺寸

     一般都能支持176X144/352X288這種尺寸,但是大一些的,640X480就有很多機子不行了,至於為什么,我也不知道。當然,這個尺寸必須和攝像頭預覽的尺寸一致,預覽的尺寸可以枚舉一下。

2、顏色空間

    根據ANdroid SDK文檔,確保所有硬件平台都支持的顏色,在攝像頭預覽輸出是YUV12,在編碼器輸入是COLOR_FormatYUV420Planar,也就是前面代碼中設置的那樣。       不過,文檔終究是文檔,否則安卓就不是安卓。

    在有的平台上,這兩個顏色格式是一樣的,攝像頭的輸出可以直接作為編碼器的輸入。也有的平台,兩個是不一樣的,前者就是YUV12,后者等於I420,需要把前者的UV分量顛倒一下。下面的代碼效率不高,可供參考。

[java]  view plain copy
  1. byte[] i420bytes = null;  
  2. private byte[] swapYV12toI420(byte[] yv12bytes, int width, int height) {  
  3.     if (i420bytes == null)  
  4.         i420bytes = new byte[yv12bytes.length];  
  5.     for (int i = 0; i < width*height; i++)  
  6.         i420bytes[i] = yv12bytes[i];  
  7.     for (int i = width*height; i < width*height + (width/2*height/2); i++)  
  8.         i420bytes[i] = yv12bytes[i + (width/2*height/2)];  
  9.     for (int i = width*height + (width/2*height/2); i < width*height + 2*(width/2*height/2); i++)  
  10.         i420bytes[i] = yv12bytes[i - (width/2*height/2)];  
  11.     return i420bytes;  
  12. }  

      這里的困難是,我不知道怎樣去判斷是否需要這個轉換。據說,Android 4.3不用再從攝像頭的PreView里面取圖像,避開了這個問題。這里有個例子,雖然我沒讀,但看起來挺厲害的樣子,應該不會有錯吧(覺厲應然)。http://bigflake.com/mediacodec/CameraToMpegTest.java.txt

 

3、輸入輸出緩沖區的格式

    SDK里並沒有規定格式,但是,這種情況H264的格式基本上就是附錄B。但是,也有比較有特色的,它就是不帶那個StartCode,就是那個0x000001,搞得把他編碼器編出來的東西送給他的解碼器,他自己都解不出來。還好,我們可以自己加。

[java]  view plain copy
  1. ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];  
  2. byte[] outData = new byte[bufferInfo.size + 3];  
  3.        outputBuffer.get(outData, 3, bufferInfo.size);  
  4. if (frameListener != null) {  
  5.     if ((outData[3]==0 && outData[4]==0 && outData[5]==1)  
  6.     || (outData[3]==0 && outData[4]==0 && outData[5]==0 && outData[6]==1))  
  7.     {  
  8.         frameListener.onFrame(outData, 3, outData.length-3, bufferInfo.flags);  
  9.     }  
  10.     else  
  11.     {  
  12.      outData[0] = 0;  
  13.      outData[1] = 0;  
  14.      outData[2] = 1;  
  15.         frameListener.onFrame(outData, 0, outData.length, bufferInfo.flags);  
  16.     }  
  17. }  


4、有時候會死在dequeueInputBuffer(-1)上面

根據SDK文檔,dequeueInputBuffer 的參數表示等待的時間(毫秒),-1表示一直等,0表示不等。按常理傳-1就行,但實際上在很多機子上會掛掉,沒辦法,還是傳0吧,丟幀總比掛掉好。當然也可以傳一個具體的毫秒數,不過沒什么大意思吧。

 
或許有用的資源:http://bigflake.com/mediacodec/
http://developer.android.com/reference/android/media/MediaCodec.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM