ArrayList<Bitmap>转视频时长偏短问题排查与encodeVideo方法优化
问题原因与优化方案
问题原因
核心问题是未为每一帧设置正确的时间戳(Presentation Time),且帧提交逻辑存在缺陷:
- 使用
InputSurface向MediaCodec提交帧时,若不指定时间戳,编码器会默认用系统当前时间作为帧的时间戳。由于循环提交Bitmap的速度远快于设定的30fps,所有帧的时间戳间隔被压缩到极小(几毫秒甚至更短)。 - 视频播放器依赖帧的时间戳计算播放时长,密集的时间戳会让播放器快速跑完所有帧,最终表现为视频时长缩短、播放速度翻倍。
- 原代码中
dequeueOutputBuffer的超时参数设为0,可能导致编码器尚未处理完当前帧就提前退出循环,部分帧的处理逻辑不完整。 - 循环结束后未向编码器发送结束信号,可能导致最后几帧未被正确写入。
优化后的encodeVideo方法
主要优化点:
- 基于帧率计算每帧的时间戳间隔(微秒级),为每一帧绑定正确的时间戳
- 通过EGL API为InputSurface的帧设置时间戳
- 调整输出缓冲的超时等待时间,确保编码器完成帧处理
- 循环结束后发送结束信号,保证所有帧被完整写入
- 补充EGL资源的初始化与释放逻辑
private static final String MIME_TYPE = "video/avc"; private static final int BIT_RATE = 3000000; private static final int FRAME_RATE = 30; private static final int I_FRAME_INTERVAL = 10; // 每帧的时间间隔(微秒):1秒=1000000微秒 private static final long FRAME_INTERVAL_US = 1_000_000 / FRAME_RATE; private EGLDisplay eglDisplay; private EGLContext eglContext; private EGLSurface eglSurface; public void encodeVideo(ArrayList<Bitmap> bitmaps) { Executor executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { if (bitmaps == null || bitmaps.isEmpty()) { return; } try { width = bitmaps.get(0).getWidth(); height = bitmaps.get(0).getHeight(); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); String fileName1 = "VID_" + timeStamp + ".mp4"; outputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), fileName1); MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL); mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); inputSurface = mediaCodec.createInputSurface(); mediaCodec.start(); // 初始化EGL环境,用于设置帧时间戳 initEGL(); mediaMuxer = new MediaMuxer(outputFile.getPath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); long presentationTimeUs = 0; for (Bitmap bitmap : bitmaps) { if (Thread.currentThread().isInterrupted()) { break; } Canvas canvas = inputSurface.lockCanvas(null); if (canvas != null) { try { canvas.drawBitmap(bitmap, 0, 0, null); } finally { // 为当前帧设置时间戳 eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeUs); inputSurface.unlockCanvasAndPost(canvas); } } // 更新下一帧的时间戳 presentationTimeUs += FRAME_INTERVAL_US; // 可选:回收Bitmap释放内存,避免OOM bitmap.recycle(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); while (true) { // 设置合理超时,等待编码器处理完成 int encoderStatus = mediaCodec.dequeueOutputBuffer(bufferInfo, 10_000); if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { break; } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { if (muxerStarted) { releaseResources(); return; } MediaFormat newFormat = mediaCodec.getOutputFormat(); videoTrackIndex = mediaMuxer.addTrack(newFormat); mediaMuxer.start(); muxerStarted = true; } else if (encoderStatus < 0) { Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus); } else { ByteBuffer encodedData = mediaCodec.getOutputBuffer(encoderStatus); if (encodedData == null) { releaseResources(); return; } if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { bufferInfo.size = 0; } if (bufferInfo.size != 0) { if (!muxerStarted) { releaseResources(); return; } encodedData.position(bufferInfo.offset); encodedData.limit(bufferInfo.offset + bufferInfo.size); mediaMuxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo); } mediaCodec.releaseOutputBuffer(encoderStatus, false); if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } } } // 发送结束信号,告知编码器无更多帧 mediaCodec.signalEndOfInputStream(); // 处理剩余的输出缓冲 MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); while (true) { int encoderStatus = mediaCodec.dequeueOutputBuffer(bufferInfo, 10_000); if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { continue; } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { continue; } else if (encoderStatus < 0) { Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus); } else { ByteBuffer encodedData = mediaCodec.getOutputBuffer(encoderStatus); if (encodedData == null) { releaseResources(); return; } if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { bufferInfo.size = 0; } if (bufferInfo.size != 0) { if (!muxerStarted) { releaseResources(); return; } encodedData.position(bufferInfo.offset); encodedData.limit(bufferInfo.offset + bufferInfo.size); mediaMuxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo); } mediaCodec.releaseOutputBuffer(encoderStatus, false); if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } } releaseResources(); ContentValues values = new ContentValues(); values.put(MediaStore.Video.Media.DATA, outputFile.getAbsolutePath()); values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4"); Uri uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values); } catch (IOException e) { releaseResources(); } finally { // 释放EGL资源 releaseEGL(); } }); } // 初始化EGL环境 private void initEGL() { eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (eglDisplay == EGL14.EGL_NO_DISPLAY) { throw new RuntimeException("eglGetDisplay failed"); } int[] version = new int[2]; if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { throw new RuntimeException("eglInitialize failed"); } int[] configAttribs = { 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, EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT, EGL14.EGL_NONE }; EGLConfig[] configs = new EGLConfig[1]; int[] numConfigs = new int[1]; if (!EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, configs.length, numConfigs, 0)) { throw new RuntimeException("eglChooseConfig failed"); } int[] contextAttribs = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE }; eglContext = EGL14.eglCreateContext(eglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttribs, 0); if (eglContext == EGL14.EGL_NO_CONTEXT) { throw new RuntimeException("eglCreateContext failed"); } eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, configs[0], inputSurface, null, 0); if (eglSurface == EGL14.EGL_NO_SURFACE) { throw new RuntimeException("eglCreateWindowSurface failed"); } if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { throw new RuntimeException("eglMakeCurrent failed"); } } // 释放EGL资源 private void releaseEGL() { if (eglDisplay != EGL14.EGL_NO_DISPLAY) { EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); if (eglSurface != EGL14.EGL_NO_SURFACE) { EGL14.eglDestroySurface(eglDisplay, eglSurface); } if (eglContext != EGL14.EGL_NO_CONTEXT) { EGL14.eglDestroyContext(eglDisplay, eglContext); } EGL14.eglTerminate(eglDisplay); } eglDisplay = EGL14.EGL_NO_DISPLAY; eglContext = EGL14.EGL_NO_CONTEXT; eglSurface = EGL14.EGL_NO_SURFACE; } // 设置帧时间戳(API 17+支持) private void eglPresentationTimeANDROID(EGLDisplay dpy, EGLSurface sur, long timeUs) { try { Method method = EGL14.class.getMethod("eglPresentationTimeANDROID", EGLDisplay.class, EGLSurface.class, long.class); method.invoke(null, dpy, sur, timeUs); } catch (Exception e) { Log.w(TAG, "eglPresentationTimeANDROID not supported", e); } }
额外注意事项
eglPresentationTimeANDROID要求API 17及以上版本,需针对低版本做兼容处理- 循环中及时回收Bitmap,避免内存溢出
- 确保应用拥有文件写入权限(Android 10及以下需
WRITE_EXTERNAL_STORAGE,Android 11+可直接使用MediaStore) - 补充EGL初始化失败的容错逻辑,避免应用崩溃
内容的提问来源于stack exchange,提问作者Lewis Jack
相关产品推荐
相关产品推荐

