问题描述
我正在尝试使用存储在设备中的位图创建一个延时视频。我正在尝试使用MediaCodec和openGl来做到这一点,如以下示例所示: https://www.sisik.eu/blog/android/media/images-to-video https://bigflake.com/mediacodec/EncodeAndMuxTest.java.txt
问题是我的代码仅产生黑框。有人知道我在哪里犯错吗?
这是我的代码:
public class BitmapToVideoEncoder {
private static final String TAG = "EncodeAndMuxTest";
private static final boolean VERBOSE = true; // 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 = 1; // 1fps
private static final int IFRAME_INTERVAL = 1; // 1 seconds between I-frames
private final ArrayList<Uri> listOfBitmapUris;
// 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;
public BitmapToVideoEncoder(ArrayList<Uri> listOfBitmapUris) {
this.listOfBitmapUris = listOfBitmapUris;
}
/**
* 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;
}
/**
* 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 < listOfBitmapUris.size(); i++) {
// Feed any pending encoder output into the muxer.
drainEncoder(false);
// Generate a new frame of input.
int textureHandle = generateSurfaceFrame(i,listOfBitmapUris.get(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.
try {
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
} catch (IOException e) {
e.printStackTrace();
}
mEncoder.configure(format,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." + mWidth + "x" + mHeight + ".mp4").toString();
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
}
}
}
}
private int generateSurfaceFrame(int frameIndex,Uri uri) {
final int[] textureHandle = new int[1];
GLES20.glGenTextures(1,textureHandle,0);
if (textureHandle[0] != 0) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false; // No pre-scaling
// Read in the resource
Bitmap bitmap = Bitmap.createScaledBitmap(
BitmapFactory.decodeFile(uri.getPath()),480,320,true);
// Bind to the texture in OpenGL
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureHandle[0]);
// Load the bitmap into the bound texture.
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,bitmap,0);
// Set filtering
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_NEAREST);
// Recycle the bitmap,since its data has been loaded into OpenGL.
bitmap.recycle();
}
if (textureHandle[0] == 0) {
throw new RuntimeException("Error loading texture.");
}
return textureHandle[0];
}
/**
* 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,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,EGL14.EGL_BLUE_SIZE,EGL14.EGL_ALPHA_SIZE,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,configs,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,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_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,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,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));
}
}
}
}
解决方法
暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!
如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。
小编邮箱:dio#foxmail.com (将#修改为@)