You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 15:59:54