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)坐标由图片的宽高决定,铺满可能会存在图片拉伸的问题
需要根据实际图片和合成的效果做调整。