-
-
Save amykhaylyshyn/fbeb212e2302dee6e5cae621b01e83d3 to your computer and use it in GitHub Desktop.
OpenGL to video on Android, 4.3 API 18
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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.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.GLES20; | |
import android.test.AndroidTestCase; | |
import android.util.Log; | |
import android.view.Surface; | |
import java.io.IOException; | |
import java.nio.ByteBuffer; | |
/** | |
* Generate an MP4 file using OpenGL ES drawing commands. Demonstrates the use of MediaMuxer | |
* and MediaCodec with Surface input. | |
* <p> | |
* This uses various features first available in Android "Jellybean" 4.3 (API 18). There is | |
* no equivalent functionality in previous releases. | |
* <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 EncodeAndMuxTest extends AndroidTestCase { | |
private static final String TAG = "EncodeAndMuxTest"; | |
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 String OUTPUT_DIR = "/sdcard/"; | |
// parameters for the encoder | |
private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding | |
private static final int FRAME_RATE = 15; // 15fps | |
private static final int IFRAME_INTERVAL = 10; // 10 seconds between I-frames | |
private static final int NUM_FRAMES = 30; // two seconds of video | |
// RGB color values for generated frames | |
private static final int TEST_R0 = 0; | |
private static final int TEST_G0 = 136; | |
private static final int TEST_B0 = 0; | |
private static final int TEST_R1 = 236; | |
private static final int TEST_G1 = 50; | |
private static final int TEST_B1 = 186; | |
// size of a frame, in pixels | |
private int mWidth = -1; | |
private int mHeight = -1; | |
// bit rate, in bits per second | |
private int mBitRate = -1; | |
// encoder / muxer state | |
private MediaCodec mEncoder; | |
private CodecInputSurface mInputSurface; | |
private MediaMuxer mMuxer; | |
private int mTrackIndex; | |
private boolean mMuxerStarted; | |
// allocate one of these up front so we don't need to do it every time | |
private MediaCodec.BufferInfo mBufferInfo; | |
/** | |
* Tests encoding of AVC video from a Surface. The output is saved as an MP4 file. | |
*/ | |
public void testEncodeVideoToMp4() { | |
// QVGA at 2Mbps | |
mWidth = 320; | |
mHeight = 240; | |
mBitRate = 2000000; | |
try { | |
prepareEncoder(); | |
mInputSurface.makeCurrent(); | |
for (int i = 0; i < NUM_FRAMES; i++) { | |
// Feed any pending encoder output into the muxer. | |
drainEncoder(false); | |
// Generate a new frame of input. | |
generateSurfaceFrame(i); | |
mInputSurface.setPresentationTime(computePresentationTimeNsec(i)); | |
// 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 " + i + " to encoder"); | |
mInputSurface.swapBuffers(); | |
} | |
// send end-of-stream to encoder, and drain remaining output | |
drainEncoder(true); | |
} finally { | |
// release encoder, muxer, and input Surface | |
releaseEncoder(); | |
} | |
// To test the result, open the file with MediaExtractor, and get the format. Pass | |
// that into the MediaCodec decoder configuration, along with a SurfaceTexture surface, | |
// and examine the output with glReadPixels. | |
} | |
/** | |
* Configures encoder and muxer state, and prepares the input Surface. | |
*/ | |
private void prepareEncoder() { | |
mBufferInfo = new MediaCodec.BufferInfo(); | |
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight); | |
// 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, mBitRate); | |
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 = OUTPUT_DIR + "test." + mWidth + "x" + mHeight + ".mp4"; | |
Log.d(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. May be called after partial / failed initialization. | |
*/ | |
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. | |
* <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. | |
*/ | |
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 | |
} | |
} | |
} | |
} | |
/** | |
* Generates a frame of data using GL commands. We have an 8-frame animation | |
* sequence that wraps around. It looks like this: | |
* <pre> | |
* 0 1 2 3 | |
* 7 6 5 4 | |
* </pre> | |
* We draw one of the eight rectangles and leave the rest set to the clear color. | |
*/ | |
private void generateSurfaceFrame(int frameIndex) { | |
frameIndex %= 8; | |
int startX, startY; | |
if (frameIndex < 4) { | |
// (0,0) is bottom-left in GL | |
startX = frameIndex * (mWidth / 4); | |
startY = mHeight / 2; | |
} else { | |
startX = (7 - frameIndex) * (mWidth / 4); | |
startY = 0; | |
} | |
GLES20.glClearColor(TEST_R0 / 255.0f, TEST_G0 / 255.0f, TEST_B0 / 255.0f, 1.0f); | |
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); | |
GLES20.glEnable(GLES20.GL_SCISSOR_TEST); | |
GLES20.glScissor(startX, startY, mWidth / 4, mHeight / 2); | |
GLES20.glClearColor(TEST_R1 / 255.0f, TEST_G1 / 255.0f, TEST_B1 / 255.0f, 1.0f); | |
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); | |
GLES20.glDisable(GLES20.GL_SCISSOR_TEST); | |
} | |
/** | |
* Generates the presentation time for frame N, in nanoseconds. | |
*/ | |
private static long computePresentationTimeNsec(int frameIndex) { | |
final long ONE_BILLION = 1000000000; | |
return frameIndex * ONE_BILLION / FRAME_RATE; | |
} | |
/** | |
* 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_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)); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment