如何使用MediaCodec把多张图片合成视频(Kotlin)

1.常规合成方式

MediaCodec 是 Android 提供的 API,用于实时编解码音视频数据。它可以用于将一组图片合成成视频。

要实现图片合成视频,首先需要将图片转换为视频帧。可以使用 MediaCodec 编码器将每张图片编码为视频帧,然后使用 MediaMuxer 将这些视频帧合成为视频文件。

import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

public class ImageToVideoConverter {
    private static final String TAG = "ImageToVideoConverter";

    private static final String MIME_TYPE = "video/avc";
    private static final int FRAME_RATE = 10;
    private static final int IFRAME_INTERVAL = 1;

    public static void convertImagesToVideo(String[] imagePaths, String outputVideoPath) {
        try {
            MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);
            if (codecInfo == null) {
                Log.e(TAG, "Unable to find codec for " + MIME_TYPE);
                return;
            }

            MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, 640, 480);
            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
            format.setInteger(MediaFormat.KEY_BIT_RATE, 500000);
            format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);

            MediaCodec codec = MediaCodec.createByCodecName(codecInfo.getName());
            codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            codec.start();

            MediaMuxer muxer = new MediaMuxer(outputVideoPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

            for (int i = 0; i < imagePaths.length; i++) {
                MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                int inputBufferIndex = codec.dequeueInputBuffer(-1);
                if (inputBufferIndex >= 0) {
                    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);
                    inputBuffer.clear();

                    Bitmap bitmap = BitmapFactory.decodeFile(imagePaths[i]);
                    if (bitmap == null) {
                        Log.e(TAG, "Failed to decode image");
                        continue;
                    }

                    inputBuffer.put(convertBitmapToByteArray(bitmap));
                    codec.queueInputBuffer(inputBufferIndex, 0, inputBuffer.position(), 0, 0);
                }

                int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0);
                if (outputBufferIndex >= 0) {
                    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferIndex);
                    muxer.writeSampleData(0, outputBuffer, bufferInfo);
                    codec.releaseOutputBuffer(outputBufferIndex, false);
                }
            }

            codec.stop();
            codec.release();
            muxer.stop();
            muxer.release();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static MediaCodecInfo selectCodec(String mimeType) {
        int numCodecs = MediaCodecList.getCodecCount();
        for (int i = 0; i < numCodecs; i++) {
            MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
            if (!codecInfo.isEncoder()) {
                continue;
            }

            String[] types = codecInfo.getSupportedTypes();
            for (String type : types) {
                if (type.equalsIgnoreCase(mimeType)) {
                    return codecInfo;
                }
            }
        }

        return null;
    }

    private static byte[] convertBitmapToByteArray(Bitmap bitmap) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
        return stream.toByteArray();
    }
}

调用方式

String[] imagePaths = {"/sdcard/image1.jpg", "/sdcard/image2.jpg", "/sdcard/image3.jpg"};
String outputVideoPath = "/sdcard/video.mp4";
ImageToVideoConverter.convertImagesToVideo(imagePaths, outputVideoPath);

2.升级版本

使用surface的方式,先把图片用canvas画布渲染,再用MediaCodec编码

fun convertImagesToVideo(imagePath: List<String>, videoPath: String, onSuccess: () -> Unit) {
        val mediaFormat =
            MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080).apply {
                setInteger(MediaFormat.KEY_BIT_RATE, 1920 * 1080 * 10)
                setInteger(MediaFormat.KEY_FRAME_RATE, 25)
                setInteger(
                    MediaFormat.KEY_COLOR_FORMAT,
                    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
                )
                setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
            }
        val muxer = MediaMuxer(videoPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        val codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC).apply {
            configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        }
        val surface = codec.createInputSurface()
        codec.start()
        val startTimeSeconds = System.currentTimeMillis() / 1000L

        var newFormat = codec.outputFormat
        var videoTrackIndex = muxer.addTrack(newFormat)

        imagePath.forEachIndexed { index, path ->
            val bitmap = BitmapFactory.decodeFile(path) ?: return@forEachIndexed
            val canvas = surface.lockCanvas(Rect(0, 0, 1920, 1080))
            canvas.drawBitmap(bitmap, null, Rect(0, 0, 1920, 1080), null)
            surface.unlockCanvasAndPost(canvas)
            var outputBufferIndex: Int
            val info = MediaCodec.BufferInfo()
            while (true) {
                outputBufferIndex = codec.dequeueOutputBuffer(info, 20 * 1000)
                Log.e("Codec", "outputBufferIndex=$outputBufferIndex")
                when {
                    outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
                        Log.e("Codec", "no output from encoder available")
                        break
                    }

                    outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                        newFormat = codec.outputFormat
                        videoTrackIndex = muxer.addTrack(newFormat)
                        Log.e("Codec", "output format changes videoTrackIndex=$videoTrackIndex")
                        muxer.start()
                    }

                    outputBufferIndex < 0 -> {
                        Log.e("Codec", "outputBufferIndex=$outputBufferIndex")
                        break
                    }

                    else -> {
                        val encodedData = codec.getOutputBuffer(outputBufferIndex)
                        if (encodedData == null) {
                            Log.e("Codec", "encoderOutputBuffer $outputBufferIndex was null")
                            break
                        }
                        if (info.size != 0) {
                            info.presentationTimeUs = (startTimeSeconds + index) * 1000L * 1000L
                            if (index == imagePath.size - 1) {
                                info.flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM
                            } else {
                                info.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME
                            }
                            Log.e("Codec", "videoTrackIndex=$videoTrackIndex info=$info")
                            if (videoTrackIndex >= 0) {
                                muxer.writeSampleData(videoTrackIndex, encodedData, info)
                            }
                        }
                        codec.releaseOutputBuffer(outputBufferIndex, false)
                    }
                }
            }
        }
        try {
            codec.stop()
            codec.release()

            muxer.stop()
            muxer.release()
            Log.e("Codec", "success...")
        } catch (e: Exception) {
            e.printStackTrace()
        }
        onSuccess()
    }

3.核心

使用MediaCodec创建虚拟图层

val surface = codec.createInputSurface()

在此Surface上使用画布渲染图片生成的bitmap

val bitmap = BitmapFactory.decodeFile(path) ?: return@forEachIndexed
val canvas = surface.lockCanvas(Rect(0, 0, 1920, 1080))
canvas.drawBitmap(bitmap, null, Rect(0, 0, 1920, 1080), null)
surface.unlockCanvasAndPost(canvas)
Rect(0, 0, 1920, 1080)坐标由图片的宽高决定,铺满可能会存在图片拉伸的问题

需要根据实际图片和合成的效果做调整。

  • 7
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会写代码的猴子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值