http://blog.csdn.net/halleyzhang3/article/details/11473961
http://www.360doc.com/content/14/0119/10/8122810_346350456.shtml
本文向你講述如何用android標準的API (MediaCodec)實現視頻的硬件編解碼。例程將從攝像頭採集視頻開始,然後進行H264編碼,再解碼,然後顯示。我將盡量講得簡短而清晰,不展示那些不相關的代碼。但是,我不建議你讀這篇文章,也不建議你開發這類應用,而應該轉而開發一些戳魚、打鳥、其樂融融的程序。好吧,下面的內容是寫給那些執迷不悟的人的,看完之後也許你會同意我的說法:Android只是一個玩具,很難指望它來做靠譜的應用。
1、從攝像頭採集視頻
可以通過攝像頭Preview的回調,來獲取視頻數據。
首先創建攝像頭,並設置參數:
- cam = Camera.open();
- cam.setPreviewDisplay(holder);
- Camera.Parameters parameters = cam.getParameters();
- parameters.setFlashMode("off"); // 無閃光燈
- parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
- parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);
- parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
- parameters.setPreviewFormat(ImageFormat.YV12);
- parameters.setPictureSize(camWidth, camHeight);
- parameters.setPreviewSize(camWidth, camHeight);
- //這兩個屬性 如果這兩個屬性設置的和真實手機的不一樣時,就會報錯
- cam.setParameters(parameters);
- buf = new byte[camWidth * camHeight * 3 / 2];
- cam.addCallbackBuffer(buf);
- cam.setPreviewCallbackWithBuffer(this);
- cam.startPreview();
setPreviewCallbackWithBuffer是很有必要的,不然每次回調系統都重新分配緩衝區,效率會很低。
在onPreviewFrame中就可以獲得原始的圖片了(當然,this 肯定要 implements PreviewCallback了)。這裏我們是把它傳給編碼器:
- public void onPreviewFrame(byte[] data, Camera camera) {
- if (frameListener != null) {
- frameListener.onFrame(data, 0, data.length, 0);
- }
- cam.addCallbackBuffer(buf);
- }
首先要初始化編碼器:
- mediaCodec = MediaCodec.createEncoderByType("Video/AVC");
- MediaFormat mediaFormat = MediaFormat.createVideoFormat(type, width, height);
- mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
- mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
- mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
- mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
- mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
- mediaCodec.start();
然後就是給他喂數據了,這裏的數據是來自攝像頭的:
- public void onFrame(byte[] buf, int offset, int length, int flag) {
- ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
- ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
- int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
- if (inputBufferIndex >= 0)
- ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
- inputBuffer.clear();
- inputBuffer.put(buf, offset, length);
- mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, 0, 0);
- }
- MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
- int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);
- while (outputBufferIndex >= 0) {
- ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
- if (frameListener != null)
- frameListener.onFrame(outputBuffer, 0, length, flag);
- mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
- outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
- }
3、解碼和顯示
首先初始化解碼器:
- mediaCodec = MediaCodec.createDecoderByType("Video/AVC");
- MediaFormat mediaFormat = MediaFormat.createVideoFormat(mime, width, height);
- mediaCodec.configure(mediaFormat, surface, null, 0);
- mediaCodec.start();
這裏通過給解碼器一個surface,解碼器就能直接顯示畫面。
然後就是處理數據了:
- public void onFrame(byte[] buf, int offset, int length, int flag) {
- ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
- int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
- if (inputBufferIndex >= 0) {
- ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
- inputBuffer.clear();
- inputBuffer.put(buf, offset, length);
- mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 1000000 / FRAME_RATE, 0);
- mCount++;
- }
- MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
- int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);
- while (outputBufferIndex >= 0) {
- mediaCodec.releaseOutputBuffer(outputBufferIndex, true);
- outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
- }
- }
好了,到現在,基本上就可以了。如果你運氣夠好,現在就能看到視頻了,比如在我的三星手機上這樣就可以了。但是,我試過幾個其他平臺,多數都不可以,總是有各種各樣的問題,如果要開發一個不依賴平臺的應用,還有很多的問題要解決。說說我遇到的一些情況:
1、視頻尺寸
一般都能支持176X144/352X288這種尺寸,但是大一些的,640X480就有很多機子不行了,至於爲什麼,我也不知道。當然,這個尺寸必須和攝像頭預覽的尺寸一致,預覽的尺寸可以枚舉一下。
2、顏色空間
根據ANdroid SDK文檔,確保所有硬件平臺都支持的顏色,在攝像頭預覽輸出是YUV12,在編碼器輸入是COLOR_FormatYUV420Planar,也就是前面代碼中設置的那樣。 不過,文檔終究是文檔,否則安卓就不是安卓。
在有的平臺上,這兩個顏色格式是一樣的,攝像頭的輸出可以直接作爲編碼器的輸入。也有的平臺,兩個是不一樣的,前者就是YUV12,後者等於I420,需要把前者的UV分量顛倒一下。下面的代碼效率不高,可供參考。
- byte[] i420bytes = null;
- private byte[] swapYV12toI420(byte[] yv12bytes, int width, int height) {
- if (i420bytes == null)
- i420bytes = new byte[yv12bytes.length];
- for (int i = 0; i < width*height; i++)
- i420bytes[i] = yv12bytes[i];
- for (int i = width*height; i < width*height + (width/2*height/2); i++)
- i420bytes[i] = yv12bytes[i + (width/2*height/2)];
- for (int i = width*height + (width/2*height/2); i < width*height + 2*(width/2*height/2); i++)
- i420bytes[i] = yv12bytes[i - (width/2*height/2)];
- return i420bytes;
- }
3、輸入輸出緩衝區的格式
SDK裏並沒有規定格式,但是,這種情況H264的格式基本上就是附錄B。但是,也有比較有特色的,它就是不帶那個StartCode,就是那個0x000001,搞得把他編碼器編出來的東西送給他的解碼器,他自己都解不出來。還好,我們可以自己加。
- ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
- byte[] outData = new byte[bufferInfo.size + 3];
- outputBuffer.get(outData, 3, bufferInfo.size);
- if (frameListener != null) {
- if ((outData[3]==0 && outData[4]==0 && outData[5]==1)
- || (outData[3]==0 && outData[4]==0 && outData[5]==0 && outData[6]==1))
- {
- frameListener.onFrame(outData, 3, outData.length-3, bufferInfo.flags);
- }
- else
- {
- outData[0] = 0;
- outData[1] = 0;
- outData[2] = 1;
- frameListener.onFrame(outData, 0, outData.length, bufferInfo.flags);
- }
- }
4、有時候會死在dequeueInputBuffer(-1)上面
根據SDK文檔,dequeueInputBuffer 的參數表示等待的時間(毫秒),-1表示一直等,0表示不等。按常理傳-1就行,但實際上在很多機子上會掛掉,沒辦法,還是傳0吧,丟幀總比掛掉好。當然也可以傳一個具體的毫秒數,不過沒什麼大意思吧。