本文向你講述如何用android標准的API (MediaCodec)實現視頻的硬件編解碼。例程將從攝像頭采集視頻開始,然后進行H264編碼,再解碼,然后顯示。我將盡量講得簡短而清晰,不展示 那些不相關的代碼。但是,我不建議你讀這篇文章,也不建議你開發這類應用,而應該轉而開發一些戳魚、打鳥、其樂融融的程序。好吧,下面的內容是寫給那些執 迷不悟的人的,看完之后也許你會同意我的說法:Android只是一個玩具,很難指望它來做靠譜的應用。
1、從攝像頭采集視頻
可以通過攝像頭Preview的回調,來獲取視頻數據。
首先創建攝像頭,並設置參數:
寬度和高度必須是攝像頭支持的尺寸,否則會報錯。要獲得所有支持的尺寸,可用getSupportedPreviewSizes,這里不再累述。據說所有的參數必須設全,漏掉一個就可能報錯,不過只是據說,我只設了幾個屬性也沒出錯。 然后就開始Preview了:
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); }
2、編碼
首先要初始化編碼器:
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); } }
queueInputBuffer第三個參數是時間戳,其實怎么寫都無所謂,只要是按時間線性增加的就可以,這里就隨便弄一個了。后面一段的代碼就是把緩 沖區給釋放掉,因為我們直接讓解碼器顯示,就不需要解碼出來的數據了,但是必須要這么釋放一下,否則解碼器始終給你留着,內存就該不夠用了。
好了,到現在,基本上就可以了。如果你運氣夠好,現在就能看到視頻了,比如在我的三星手機上這樣就可以了。但是,我試過幾個其他平台,多數都不可以,總是有各種各樣的問題,如果要開發一個不依賴平台的應用,還有很多的問題要解決。說說我遇到的一些情況:
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; }
這里的困難是,我不知道怎樣去判斷是否需要這個轉換。據說,Android 4.3不用再從攝像頭的PreView里面取圖像,避開了這個問題。這里有個例子,雖然我沒讀,但看起來挺厲害的樣子,應該不會有錯吧(覺厲應然)。http://bigflake.com/mediacodec/CameraToMpegTest.java.txt,這個文件的代碼附在本文最后。
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吧,丟幀總比掛掉好。當然也可以傳一個具體的毫秒數,不過沒什么大意思吧。
在遇到上述的問題之后,我給出了我的感慨:Android是一個玩具。
/* * Copyright 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media.cts; import android.graphics.SurfaceTexture; import android.hardware.Camera; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.MediaMuxer; import android.opengl.EGL14; import android.opengl.EGLConfig; import android.opengl.EGLContext; import android.opengl.EGLDisplay; import android.opengl.EGLExt; import android.opengl.EGLSurface; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.opengl.Matrix; import android.os.Environment; import android.test.AndroidTestCase; import android.util.Log; import android.view.Surface; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; //20131106: removed unnecessary glFinish(), removed hard-coded "/sdcard" //20131205: added alpha to EGLConfig //20131210: demonstrate un-bind and re-bind of texture, for apps with shared EGL contexts //20140123: correct error checks on glGet*Location() and program creation (they don't set error) /** * Record video from the camera preview and encode it as an MP4 file. Demonstrates the use * of MediaMuxer and MediaCodec with Camera input. Does not record audio. * <p> * Generally speaking, it's better to use MediaRecorder for this sort of thing. This example * demonstrates one possible advantage: editing of video as it's being encoded. A GLES 2.0 * fragment shader is used to perform a silly color tweak every 15 frames. * <p> * This uses various features first available in Android "Jellybean" 4.3 (API 18). There is * no equivalent functionality in previous releases. (You can send the Camera preview to a * byte buffer with a fully-specified format, but MediaCodec encoders want different input * formats on different devices, and this use case wasn't well exercised in CTS pre-4.3.) * <p> * The output file will be something like "/sdcard/test.640x480.mp4". * <p> * (This was derived from bits and pieces of CTS tests, and is packaged as such, but is not * currently part of CTS.) */ public class CameraToMpegTest extends AndroidTestCase { private static final String TAG = "CameraToMpegTest"; private static final boolean VERBOSE = false; // lots of logging // where to put the output file (note: /sdcard requires WRITE_EXTERNAL_STORAGE permission) private static final File OUTPUT_DIR = Environment.getExternalStorageDirectory(); // parameters for the encoder private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding private static final int FRAME_RATE = 30; // 30fps private static final int IFRAME_INTERVAL = 5; // 5 seconds between I-frames private static final long DURATION_SEC = 8; // 8 seconds of video // Fragment shader that swaps color channels around. private static final String SWAPPED_FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + "varying vec2 vTextureCoord;\n" + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" + " gl_FragColor = texture2D(sTexture, vTextureCoord).gbra;\n" + "}\n"; // encoder / muxer state private MediaCodec mEncoder; private CodecInputSurface mInputSurface; private MediaMuxer mMuxer; private int mTrackIndex; private boolean mMuxerStarted; // camera state private Camera mCamera; private SurfaceTextureManager mStManager; // allocate one of these up front so we don't need to do it every time private MediaCodec.BufferInfo mBufferInfo; /** test entry point */ public void testEncodeCameraToMp4() throws Throwable { CameraToMpegWrapper.runTest(this); } /** * Wraps encodeCameraToMpeg(). This is necessary because SurfaceTexture will try to use * the looper in the current thread if one exists, and the CTS tests create one on the * test thread. * * The wrapper propagates exceptions thrown by the worker thread back to the caller. */ private static class CameraToMpegWrapper implements Runnable { private Throwable mThrowable; private CameraToMpegTest mTest; private CameraToMpegWrapper(CameraToMpegTest test) { mTest = test; } @Override public void run() { try { mTest.encodeCameraToMpeg(); } catch (Throwable th) { mThrowable = th; } } /** Entry point. */ public static void runTest(CameraToMpegTest obj) throws Throwable { CameraToMpegWrapper wrapper = new CameraToMpegWrapper(obj); Thread th = new Thread(wrapper, "codec test"); th.start(); th.join(); if (wrapper.mThrowable != null) { throw wrapper.mThrowable; } } } /** * Tests encoding of AVC video from Camera input. The output is saved as an MP4 file. */ private void encodeCameraToMpeg() { // arbitrary but popular values int encWidth = 640; int encHeight = 480; int encBitRate = 6000000; // Mbps Log.d(TAG, MIME_TYPE + " output " + encWidth + "x" + encHeight + " @" + encBitRate); try { prepareCamera(encWidth, encHeight); prepareEncoder(encWidth, encHeight, encBitRate); mInputSurface.makeCurrent(); prepareSurfaceTexture(); mCamera.startPreview(); long startWhen = System.nanoTime(); long desiredEnd = startWhen + DURATION_SEC * 1000000000L; SurfaceTexture st = mStManager.getSurfaceTexture(); int frameCount = 0; while (System.nanoTime() < desiredEnd) { // Feed any pending encoder output into the muxer. drainEncoder(false); // Switch up the colors every 15 frames. Besides demonstrating the use of // fragment shaders for video editing, this provides a visual indication of // the frame rate: if the camera is capturing at 15fps, the colors will change // once per second. if ((frameCount % 15) == 0) { String fragmentShader = null; if ((frameCount & 0x01) != 0) { fragmentShader = SWAPPED_FRAGMENT_SHADER; } mStManager.changeFragmentShader(fragmentShader); } frameCount++; // Acquire a new frame of input, and render it to the Surface. If we had a // GLSurfaceView we could switch EGL contexts and call drawImage() a second // time to render it on screen. The texture can be shared between contexts by // passing the GLSurfaceView's EGLContext as eglCreateContext()'s share_context // argument. mStManager.awaitNewImage(); mStManager.drawImage(); // Set the presentation time stamp from the SurfaceTexture's time stamp. This // will be used by MediaMuxer to set the PTS in the video. if (VERBOSE) { Log.d(TAG, "present: " + ((st.getTimestamp() - startWhen) / 1000000.0) + "ms"); } mInputSurface.setPresentationTime(st.getTimestamp()); // Submit it to the encoder. The eglSwapBuffers call will block if the input // is full, which would be bad if it stayed full until we dequeued an output // buffer (which we can't do, since we're stuck here). So long as we fully drain // the encoder before supplying additional input, the system guarantees that we // can supply another frame without blocking. if (VERBOSE) Log.d(TAG, "sending frame to encoder"); mInputSurface.swapBuffers(); } // send end-of-stream to encoder, and drain remaining output drainEncoder(true); } finally { // release everything we grabbed releaseCamera(); releaseEncoder(); releaseSurfaceTexture(); } } /** * Configures Camera for video capture. Sets mCamera. * <p> * Opens a Camera and sets parameters. Does not start preview. */ private void prepareCamera(int encWidth, int encHeight) { if (mCamera != null) { throw new RuntimeException("camera already initialized"); } Camera.CameraInfo info = new Camera.CameraInfo(); // Try to find a front-facing camera (e.g. for videoconferencing). int numCameras = Camera.getNumberOfCameras(); for (int i = 0; i < numCameras; i++) { Camera.getCameraInfo(i, info); if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { mCamera = Camera.open(i); break; } } if (mCamera == null) { Log.d(TAG, "No front-facing camera found; opening default"); mCamera = Camera.open(); // opens first back-facing camera } if (mCamera == null) { throw new RuntimeException("Unable to open camera"); } Camera.Parameters parms = mCamera.getParameters(); choosePreviewSize(parms, encWidth, encHeight); // leave the frame rate set to default mCamera.setParameters(parms); Camera.Size size = parms.getPreviewSize(); Log.d(TAG, "Camera preview size is " + size.width + "x" + size.height); } /** * Attempts to find a preview size that matches the provided width and height (which * specify the dimensions of the encoded video). If it fails to find a match it just * uses the default preview size. * <p> * TODO: should do a best-fit match. */ private static void choosePreviewSize(Camera.Parameters parms, int width, int height) { // We should make sure that the requested MPEG size is less than the preferred // size, and has the same aspect ratio. Camera.Size ppsfv = parms.getPreferredPreviewSizeForVideo(); if (VERBOSE && ppsfv != null) { Log.d(TAG, "Camera preferred preview size for video is " + ppsfv.width + "x" + ppsfv.height); } for (Camera.Size size : parms.getSupportedPreviewSizes()) { if (size.width == width && size.height == height) { parms.setPreviewSize(width, height); return; } } Log.w(TAG, "Unable to set preview size to " + width + "x" + height); if (ppsfv != null) { parms.setPreviewSize(ppsfv.width, ppsfv.height); } } /** * Stops camera preview, and releases the camera to the system. */ private void releaseCamera() { if (VERBOSE) Log.d(TAG, "releasing camera"); if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); mCamera = null; } } /** * Configures SurfaceTexture for camera preview. Initializes mStManager, and sets the * associated SurfaceTexture as the Camera's "preview texture". * <p> * Configure the EGL surface that will be used for output before calling here. */ private void prepareSurfaceTexture() { mStManager = new SurfaceTextureManager(); SurfaceTexture st = mStManager.getSurfaceTexture(); try { mCamera.setPreviewTexture(st); } catch (IOException ioe) { throw new RuntimeException("setPreviewTexture failed", ioe); } } /** * Releases the SurfaceTexture. */ private void releaseSurfaceTexture() { if (mStManager != null) { mStManager.release(); mStManager = null; } } /** * Configures encoder and muxer state, and prepares the input Surface. Initializes * mEncoder, mMuxer, mInputSurface, mBufferInfo, mTrackIndex, and mMuxerStarted. */ private void prepareEncoder(int width, int height, int bitRate) { mBufferInfo = new MediaCodec.BufferInfo(); MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, 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_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); if (VERBOSE) 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. // // If you want to have two EGL contexts -- one for display, one for recording -- // you will likely want to defer instantiation of CodecInputSurface until after the // "display" EGL context is created, then modify the eglCreateContext call to // take eglGetCurrentContext() as the share_context argument. mEncoder = MediaCodec.createEncoderByType(MIME_TYPE); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mInputSurface = new CodecInputSurface(mEncoder.createInputSurface()); mEncoder.start(); // Output filename. Ideally this would use Context.getFilesDir() rather than a // hard-coded output directory. String outputPath = new File(OUTPUT_DIR, "test." + width + "x" + height + ".mp4").toString(); Log.i(TAG, "Output file is " + outputPath); // Create a MediaMuxer. We can't add the video track and start() the muxer here, // because our MediaFormat doesn't have the Magic Goodies. These can only be // obtained from the encoder after it has started processing data. // // We're not actually interested in multiplexing audio. We just want to convert // the raw H.264 elementary stream we get from MediaCodec into a .mp4 file. try { mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); } catch (IOException ioe) { throw new RuntimeException("MediaMuxer creation failed", ioe); } mTrackIndex = -1; mMuxerStarted = false; } /** * Releases encoder resources. */ private void releaseEncoder() { if (VERBOSE) Log.d(TAG, "releasing encoder objects"); if (mEncoder != null) { mEncoder.stop(); mEncoder.release(); mEncoder = null; } if (mInputSurface != null) { mInputSurface.release(); mInputSurface = null; } if (mMuxer != null) { mMuxer.stop(); mMuxer.release(); mMuxer = null; } } /** * Extracts all pending data from the encoder and forwards it to the muxer. * <p> * If endOfStream is not set, this returns when there is no more data to drain. If it * is set, we send EOS to the encoder, and then iterate until we see EOS on the output. * Calling this with endOfStream set should be done once, right before stopping the muxer. * <p> * We're just using the muxer to get a .mp4 file (instead of a raw H.264 stream). We're * not recording audio. */ private void drainEncoder(boolean endOfStream) { final int TIMEOUT_USEC = 10000; if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")"); if (endOfStream) { if (VERBOSE) Log.d(TAG, "sending EOS to encoder"); mEncoder.signalEndOfInputStream(); } ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers(); 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 { if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS"); } } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { // not expected for an encoder encoderOutputBuffers = mEncoder.getOutputBuffers(); } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // should happen before receiving buffers, and should only happen once if (mMuxerStarted) { throw new RuntimeException("format changed twice"); } MediaFormat newFormat = mEncoder.getOutputFormat(); Log.d(TAG, "encoder output format changed: " + newFormat); // now that we have the Magic Goodies, start the muxer mTrackIndex = mMuxer.addTrack(newFormat); mMuxer.start(); mMuxerStarted = true; } else if (encoderStatus < 0) { Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus); // let's ignore it } else { ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; if (encodedData == null) { throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); } 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. if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG"); mBufferInfo.size = 0; } if (mBufferInfo.size != 0) { if (!mMuxerStarted) { throw new RuntimeException("muxer hasn't started"); } // adjust the ByteBuffer values to match BufferInfo (not needed?) encodedData.position(mBufferInfo.offset); encodedData.limit(mBufferInfo.offset + mBufferInfo.size); mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo); if (VERBOSE) Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer"); } mEncoder.releaseOutputBuffer(encoderStatus, false); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { if (!endOfStream) { Log.w(TAG, "reached end of stream unexpectedly"); } else { if (VERBOSE) Log.d(TAG, "end of stream reached"); } break; // out of while } } } } /** * Holds state associated with a Surface used for MediaCodec encoder input. * <p> * The constructor takes a Surface obtained from MediaCodec.createInputSurface(), and uses * that to create an EGL window surface. Calls to eglSwapBuffers() cause a frame of data to * be sent to the video encoder. * <p> * This object owns the Surface -- releasing this will release the Surface too. */ private static class CodecInputSurface { private static final int EGL_RECORDABLE_ANDROID = 0x3142; private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY; private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT; private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE; private Surface mSurface; /** * Creates a CodecInputSurface from a Surface. */ public CodecInputSurface(Surface surface) { if (surface == null) { throw new NullPointerException(); } mSurface = surface; eglSetup(); } /** * Prepares EGL. We want a GLES 2.0 context and a surface that supports recording. */ private void eglSetup() { mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { throw new RuntimeException("unable to get EGL14 display"); } int[] version = new int[2]; if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { throw new RuntimeException("unable to initialize EGL14"); } // Configure EGL for recording and OpenGL ES 2.0. int[] attribList = { EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, EGL_RECORDABLE_ANDROID, 1, EGL14.EGL_NONE }; EGLConfig[] configs = new EGLConfig[1]; int[] numConfigs = new int[1]; EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0); checkEglError("eglCreateContext RGB888+recordable ES2"); // Configure context for OpenGL ES 2.0. int[] attrib_list = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE }; mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0); checkEglError("eglCreateContext"); // Create a window surface, and attach it to the Surface we received. int[] surfaceAttribs = { EGL14.EGL_NONE }; mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface, surfaceAttribs, 0); checkEglError("eglCreateWindowSurface"); } /** * Discards all resources held by this class, notably the EGL context. Also releases the * Surface that was passed to our constructor. */ public void release() { if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface); EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); EGL14.eglReleaseThread(); EGL14.eglTerminate(mEGLDisplay); } mSurface.release(); mEGLDisplay = EGL14.EGL_NO_DISPLAY; mEGLContext = EGL14.EGL_NO_CONTEXT; mEGLSurface = EGL14.EGL_NO_SURFACE; mSurface = null; } /** * Makes our EGL context and surface current. */ public void makeCurrent() { EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext); checkEglError("eglMakeCurrent"); } /** * Calls eglSwapBuffers. Use this to "publish" the current frame. */ public boolean swapBuffers() { boolean result = EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface); checkEglError("eglSwapBuffers"); return result; } /** * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds. */ public void setPresentationTime(long nsecs) { EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs); checkEglError("eglPresentationTimeANDROID"); } /** * Checks for EGL errors. Throws an exception if one is found. */ private void checkEglError(String msg) { int error; if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error)); } } } /** * Manages a SurfaceTexture. Creates SurfaceTexture and TextureRender objects, and provides * functions that wait for frames and render them to the current EGL surface. * <p> * The SurfaceTexture can be passed to Camera.setPreviewTexture() to receive camera output. */ private static class SurfaceTextureManager implements SurfaceTexture.OnFrameAvailableListener { private SurfaceTexture mSurfaceTexture; private CameraToMpegTest.STextureRender mTextureRender; private Object mFrameSyncObject = new Object(); // guards mFrameAvailable private boolean mFrameAvailable; /** * Creates instances of TextureRender and SurfaceTexture. */ public SurfaceTextureManager() { mTextureRender = new CameraToMpegTest.STextureRender(); mTextureRender.surfaceCreated(); if (VERBOSE) Log.d(TAG, "textureID=" + mTextureRender.getTextureId()); mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId()); // This doesn't work if this object is created on the thread that CTS started for // these test cases. // // The CTS-created thread has a Looper, and the SurfaceTexture constructor will // create a Handler that uses it. The "frame available" message is delivered // there, but since we're not a Looper-based thread we'll never see it. For // this to do anything useful, OutputSurface must be created on a thread without // a Looper, so that SurfaceTexture uses the main application Looper instead. // // Java language note: passing "this" out of a constructor is generally unwise, // but we should be able to get away with it here. mSurfaceTexture.setOnFrameAvailableListener(this); } public void release() { // this causes a bunch of warnings that appear harmless but might confuse someone: // W BufferQueue: [unnamed-3997-2] cancelBuffer: BufferQueue has been abandoned! //mSurfaceTexture.release(); mTextureRender = null; mSurfaceTexture = null; } /** * Returns the SurfaceTexture. */ public SurfaceTexture getSurfaceTexture() { return mSurfaceTexture; } /** * Replaces the fragment shader. */ public void changeFragmentShader(String fragmentShader) { mTextureRender.changeFragmentShader(fragmentShader); } /** * Latches the next buffer into the texture. Must be called from the thread that created * the OutputSurface object. */ public void awaitNewImage() { final int TIMEOUT_MS = 2500; synchronized (mFrameSyncObject) { while (!mFrameAvailable) { try { // Wait for onFrameAvailable() to signal us. Use a timeout to avoid // stalling the test if it doesn't arrive. mFrameSyncObject.wait(TIMEOUT_MS); if (!mFrameAvailable) { // TODO: if "spurious wakeup", continue while loop throw new RuntimeException("Camera frame wait timed out"); } } catch (InterruptedException ie) { // shouldn't happen throw new RuntimeException(ie); } } mFrameAvailable = false; } // Latch the data. mTextureRender.checkGlError("before updateTexImage"); mSurfaceTexture.updateTexImage(); } /** * Draws the data from SurfaceTexture onto the current EGL surface. */ public void drawImage() { mTextureRender.drawFrame(mSurfaceTexture); } @Override public void onFrameAvailable(SurfaceTexture st) { if (VERBOSE) Log.d(TAG, "new frame available"); synchronized (mFrameSyncObject) { if (mFrameAvailable) { throw new RuntimeException("mFrameAvailable already set, frame could be dropped"); } mFrameAvailable = true; mFrameSyncObject.notifyAll(); } } } /** * Code for rendering a texture onto a surface using OpenGL ES 2.0. */ private static class STextureRender { private static final int FLOAT_SIZE_BYTES = 4; private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; private final float[] mTriangleVerticesData = { // X, Y, Z, U, V -1.0f, -1.0f, 0, 0.f, 0.f, 1.0f, -1.0f, 0, 1.f, 0.f, -1.0f, 1.0f, 0, 0.f, 1.f, 1.0f, 1.0f, 0, 1.f, 1.f, }; private FloatBuffer mTriangleVertices; private static final String VERTEX_SHADER = "uniform mat4 uMVPMatrix;\n" + "uniform mat4 uSTMatrix;\n" + "attribute vec4 aPosition;\n" + "attribute vec4 aTextureCoord;\n" + "varying vec2 vTextureCoord;\n" + "void main() {\n" + " gl_Position = uMVPMatrix * aPosition;\n" + " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + "}\n"; private static final String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + // highp here doesn't seem to matter "varying vec2 vTextureCoord;\n" + "uniform samplerExternalOES sTexture;\n" + "void main() {\n" + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + "}\n"; private float[] mMVPMatrix = new float[16]; private float[] mSTMatrix = new float[16]; private int mProgram; private int mTextureID = -12345; private int muMVPMatrixHandle; private int muSTMatrixHandle; private int maPositionHandle; private int maTextureHandle; public STextureRender() { mTriangleVertices = ByteBuffer.allocateDirect( mTriangleVerticesData.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()).asFloatBuffer(); mTriangleVertices.put(mTriangleVerticesData).position(0); Matrix.setIdentityM(mSTMatrix, 0); } public int getTextureId() { return mTextureID; } public void drawFrame(SurfaceTexture st) { checkGlError("onDrawFrame start"); st.getTransformMatrix(mSTMatrix); // (optional) clear to green so we can see if we're failing to set pixels GLES20.glClearColor(0.0f, 1.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); GLES20.glUseProgram(mProgram); checkGlError("glUseProgram"); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); checkGlError("glVertexAttribPointer maPosition"); GLES20.glEnableVertexAttribArray(maPositionHandle); checkGlError("glEnableVertexAttribArray maPositionHandle"); mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); checkGlError("glVertexAttribPointer maTextureHandle"); GLES20.glEnableVertexAttribArray(maTextureHandle); checkGlError("glEnableVertexAttribArray maTextureHandle"); Matrix.setIdentityM(mMVPMatrix, 0); GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0); GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); checkGlError("glDrawArrays"); // IMPORTANT: on some devices, if you are sharing the external texture between two // contexts, one context may not see updates to the texture unless you un-bind and // re-bind it. If you're not using shared EGL contexts, you don't need to bind // texture 0 here. GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); } /** * Initializes GL state. Call this after the EGL surface has been created and made current. */ public void surfaceCreated() { mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); if (mProgram == 0) { throw new RuntimeException("failed creating program"); } maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); checkLocation(maPositionHandle, "aPosition"); maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); checkLocation(maTextureHandle, "aTextureCoord"); muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); checkLocation(muMVPMatrixHandle, "uMVPMatrix"); muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); checkLocation(muSTMatrixHandle, "uSTMatrix"); int[] textures = new int[1]; GLES20.glGenTextures(1, textures, 0); mTextureID = textures[0]; GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); checkGlError("glBindTexture mTextureID"); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); checkGlError("glTexParameter"); } /** * Replaces the fragment shader. Pass in null to reset to default. */ public void changeFragmentShader(String fragmentShader) { if (fragmentShader == null) { fragmentShader = FRAGMENT_SHADER; } GLES20.glDeleteProgram(mProgram); mProgram = createProgram(VERTEX_SHADER, fragmentShader); if (mProgram == 0) { throw new RuntimeException("failed creating program"); } } private int loadShader(int shaderType, String source) { int shader = GLES20.glCreateShader(shaderType); checkGlError("glCreateShader type=" + shaderType); GLES20.glShaderSource(shader, source); GLES20.glCompileShader(shader); int[] compiled = new int[1]; GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); if (compiled[0] == 0) { Log.e(TAG, "Could not compile shader " + shaderType + ":"); Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader)); GLES20.glDeleteShader(shader); shader = 0; } return shader; } private int createProgram(String vertexSource, String fragmentSource) { int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); if (vertexShader == 0) { return 0; } int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); if (pixelShader == 0) { return 0; } int program = GLES20.glCreateProgram(); if (program == 0) { Log.e(TAG, "Could not create program"); } GLES20.glAttachShader(program, vertexShader); checkGlError("glAttachShader"); GLES20.glAttachShader(program, pixelShader); checkGlError("glAttachShader"); GLES20.glLinkProgram(program); int[] linkStatus = new int[1]; GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); if (linkStatus[0] != GLES20.GL_TRUE) { Log.e(TAG, "Could not link program: "); Log.e(TAG, GLES20.glGetProgramInfoLog(program)); GLES20.glDeleteProgram(program); program = 0; } return program; } public void checkGlError(String op) { int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { Log.e(TAG, op + ": glError " + error); throw new RuntimeException(op + ": glError " + error); } } public static void checkLocation(int location, String label) { if (location < 0) { throw new RuntimeException("Unable to locate '" + label + "' in program"); } } } }