如何在Android中从图像数组创建视频?

37
我想调用一个函数,利用图像列表生成视频,并将其保存到设备本地:
public void CreateAndSaveVideoFile(List<Bitmap> MyBitmapArray)
{
   // ..
}

试验:

从当前目录中的所有JPEG文件创建MPEG-4文件:

mencoder mf://*.jpg -mf w=800:h=600:fps=25:type=jpg -ovc lavc \ -lavcopts vcodec=mpeg4:mbd=2:trell -oac copy -o output.avi

我不知道如何在Java/Android项目中使用上面的方法..

有人能帮助指导我或者提供一种方法来完成我的任务吗?谢谢。


1
不要先制作帧的位图。也不要将它们放入列表中,因为很快你就会耗尽内存。直接将帧写入文件即可。 - greenapps
请比较帧大小/长度与位图所需内存大小并报告。如果您想使用列表,则最好将帧放入其中。只需比较所需的内存即可。 - greenapps
在按钮点击RecordVideo时,@greenapps,您希望我开始在设备本地保存ReceivedImages。您是否推荐使用保存和读取位图/图像的方式?我将继续实现保存单个图像并向您报告其大小。 - Khalil Khalaf
@ greenapps 你好,我仍然无法成功保存图像。这个链接是否有帮助?https://s11.postimg.org/4a1kdregz/Capture.png - Khalil Khalaf
1
托管一个服务,其输入为zip(照片),输出为MP4视频文件名。当调用时,它会解压缩zip中的照片-使用解压后的媒体作为输入调用ffmpeg或其他工具。输出文件是MP4。这是通用的媒体复用服务,可能可以安装为Node lib并进行最小修订。您不需要编写它。当服务器上准备好MP4时,它将被POST到CDN(提供MP4媒体)。然后客户端从CDN请求MP4。选择此架构的原因是,服务器是从移动设备捕获的媒体创建/托管/提供MP4的更好位置。 - Robert Rowntree
例如:https://dev59.com/FWQn5IYBdhLWcg3wn4RS - Robert Rowntree
7个回答

36
你可以使用 jcodec 中的 SequenceEncoder 来将一系列图像转换为 MP4 文件。
示例代码:
import org.jcodec.api.awt.SequenceEncoder;
...
SequenceEncoder enc = new SequenceEncoder(new File("filename"));
// GOP size will be supported in 0.2
// enc.getEncoder().setKeyInterval(25);
for(...) {
    BufferedImage image = ... // Obtain an image to encode
    enc.encodeImage(image);
}
enc.finish();

这是一个 Java 库,因此很容易将其导入到 Android 项目中,无需像 ffmpeg 一样使用 NDK。

请参考 http://jcodec.org/ 获取示例代码和下载链接。


太棒了,看起来就是我需要的东西。我会试试看的,谢谢! - Khalil Khalaf
@KhalilKhalaf,您是否成功让它工作了?我正在尝试同样的代码,但生成了一个损坏的.MP4文件。 - nishant1000
@nishant1000 这个功能被取消了,我被转移到其他工作上。在执行JCodec之前,您是否尝试过另一个答案中的“Bitmap to BufferedImage”方法来处理图像? - Khalil Khalaf
@Abhishek,我们能否在视频中添加音频和转场动画,同时在视频帧之间进行切换? - Mayank Pandya
Java AWT 在 Android 上似乎不可用,因此我不知道 BufferedImage 是否是一个可行的解决方案。 - caitcoo0odes
我认为应该明确强调,正如库开发人员所说,对于编码效率/速度的期望值应该设置得非常低。 - Antonio

18

在这里,Stanislav Vitvitskyy演示了如何使用JCodec请点击此处查看。

public static void main(String[] args) throws IOException {
    SequenceEncoder encoder = new SequenceEncoder(new File("video.mp4"));
    for (int i = 1; i < 100; i++) {
        BufferedImage bi = ImageIO.read(new File(String.format("img%08d.png", i)));
        encoder.encodeImage(bi);
        }
    encoder.finish();}
现在要将您的位图转换为BufferedImage,您可以使用此类:
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.io.InputStream;

/**
  * Utility class for loading windows bitmap files
  * <p>
  * Based on code from author Abdul Bezrati and Pepijn Van Eeckhoudt
  */
public class BitmapLoader {

/**
 * Static method to load a bitmap file based on the filename passed in.
 * Based on the bit count, this method will either call the 8 or 24 bit
 * bitmap reader methods
 *
 * @param file The name of the bitmap file to read
 * @throws IOException
 * @return A BufferedImage of the bitmap
 */
public static BufferedImage loadBitmap(String file) throws IOException {
    BufferedImage image;
    InputStream input = null;
    try {
        input = ResourceRetriever.getResourceAsStream(file);

        int bitmapFileHeaderLength = 14;
        int bitmapInfoHeaderLength = 40;

        byte bitmapFileHeader[] = new byte[bitmapFileHeaderLength];
        byte bitmapInfoHeader[] = new byte[bitmapInfoHeaderLength];

        input.read(bitmapFileHeader, 0, bitmapFileHeaderLength);
        input.read(bitmapInfoHeader, 0, bitmapInfoHeaderLength);

        int nSize = bytesToInt(bitmapFileHeader, 2);
        int nWidth = bytesToInt(bitmapInfoHeader, 4);
        int nHeight = bytesToInt(bitmapInfoHeader, 8);
        int nBiSize = bytesToInt(bitmapInfoHeader, 0);
        int nPlanes = bytesToShort(bitmapInfoHeader, 12);
        int nBitCount = bytesToShort(bitmapInfoHeader, 14);
        int nSizeImage = bytesToInt(bitmapInfoHeader, 20);
        int nCompression = bytesToInt(bitmapInfoHeader, 16);
        int nColoursUsed = bytesToInt(bitmapInfoHeader, 32);
        int nXPixelsMeter = bytesToInt(bitmapInfoHeader, 24);
        int nYPixelsMeter = bytesToInt(bitmapInfoHeader, 28);
        int nImportantColours = bytesToInt(bitmapInfoHeader, 36);

        if (nBitCount == 24) {
            image = read24BitBitmap(nSizeImage, nHeight, nWidth, input);
        } else if (nBitCount == 8) {
            image = read8BitBitmap(nColoursUsed, nBitCount, nSizeImage, nWidth, nHeight, input);
        } else {
            System.out.println("Not a 24-bit or 8-bit Windows Bitmap, aborting...");
            image = null;
        }
    } finally {
        try {
            if (input != null)
                input.close();
        } catch (IOException e) {
        }
    }
    return image;
}

/**
 * Static method to read a 8 bit bitmap
 *
 * @param nColoursUsed Number of colors used
 * @param nBitCount The bit count
 * @param nSizeImage The size of the image in bytes
 * @param nWidth The width of the image
 * @param input The input stream corresponding to the image
 * @throws IOException
 * @return A BufferedImage of the bitmap
 */
private static BufferedImage read8BitBitmap(int nColoursUsed, int nBitCount, int nSizeImage, int nWidth, int nHeight, InputStream input) throws IOException {
    int nNumColors = (nColoursUsed > 0) ? nColoursUsed : (1 & 0xff) << nBitCount;

    if (nSizeImage == 0) {
        nSizeImage = ((((nWidth * nBitCount) + 31) & ~31) >> 3);
        nSizeImage *= nHeight;
    }

    int npalette[] = new int[nNumColors];
    byte bpalette[] = new byte[nNumColors * 4];
    readBuffer(input, bpalette);
    int nindex8 = 0;

    for (int n = 0; n < nNumColors; n++) {
        npalette[n] = (255 & 0xff) << 24 |
                (bpalette[nindex8 + 2] & 0xff) << 16 |
                (bpalette[nindex8 + 1] & 0xff) << 8 |
                (bpalette[nindex8 + 0] & 0xff);

        nindex8 += 4;
    }

    int npad8 = (nSizeImage / nHeight) - nWidth;
    BufferedImage bufferedImage = new BufferedImage(nWidth, nHeight, BufferedImage.TYPE_INT_ARGB);
    DataBufferInt dataBufferByte = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer());
    int[][] bankData = dataBufferByte.getBankData();
    byte bdata[] = new byte[(nWidth + npad8) * nHeight];

    readBuffer(input, bdata);
    nindex8 = 0;

    for (int j8 = nHeight - 1; j8 >= 0; j8--) {
        for (int i8 = 0; i8 < nWidth; i8++) {
            bankData[0][j8 * nWidth + i8] = npalette[((int) bdata[nindex8] & 0xff)];
            nindex8++;
        }
        nindex8 += npad8;
    }

    return bufferedImage;
}

/**
 * Static method to read a 24 bit bitmap
 *
 * @param nSizeImage size of the image  in bytes
 * @param nHeight The height of the image
 * @param nWidth The width of the image
 * @param input The input stream corresponding to the image
 * @throws IOException
 * @return A BufferedImage of the bitmap
 */
private static BufferedImage read24BitBitmap(int nSizeImage, int nHeight, int nWidth, InputStream input) throws IOException {
    int npad = (nSizeImage / nHeight) - nWidth * 3;
    if (npad == 4 || npad < 0)
        npad = 0;
    int nindex = 0;
    BufferedImage bufferedImage = new BufferedImage(nWidth, nHeight, BufferedImage.TYPE_4BYTE_ABGR);
    DataBufferByte dataBufferByte = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer());
    byte[][] bankData = dataBufferByte.getBankData();
    byte brgb[] = new byte[(nWidth + npad) * 3 * nHeight];

    readBuffer(input, brgb);

    for (int j = nHeight - 1; j >= 0; j--) {
        for (int i = 0; i < nWidth; i++) {
            int base = (j * nWidth + i) * 4;
            bankData[0][base] = (byte) 255;
            bankData[0][base + 1] = brgb[nindex];
            bankData[0][base + 2] = brgb[nindex + 1];
            bankData[0][base + 3] = brgb[nindex + 2];
            nindex += 3;
        }
        nindex += npad;
    }

    return bufferedImage;
}

/**
 * Converts bytes to an int
 *
 * @param bytes An array of bytes
 * @param index
 * @returns A int representation of the bytes
 */
private static int bytesToInt(byte[] bytes, int index) {
    return (bytes[index + 3] & 0xff) << 24 |
            (bytes[index + 2] & 0xff) << 16 |
            (bytes[index + 1] & 0xff) << 8 |
            bytes[index + 0] & 0xff;
}

/**
 * Converts bytes to a short
 *
 * @param bytes An array of bytes
 * @param index
 * @returns A short representation of the bytes
 */
private static short bytesToShort(byte[] bytes, int index) {
    return (short) (((bytes[index + 1] & 0xff) << 8) |
            (bytes[index + 0] & 0xff));
}

/**
 * Reads the buffer
 *
 * @param in An InputStream
 * @param buffer An array of bytes
 * @throws IOException
 */
private static void readBuffer(InputStream in, byte[] buffer) throws IOException {
    int bytesRead = 0;
    int bytesToRead = buffer.length;
    while (bytesToRead > 0) {
        int read = in.read(buffer, bytesRead, bytesToRead);
        bytesRead += read;
        bytesToRead -= read;
    }
}
}

13
如果您的应用程序的最低Android SDK版本大于或等于16(Android 4.1),则使用Android Media Codec API是最好的视频编码方式。
Android 4.3 API开始。

在编码视频时,Android 4.1(SDK 16)要求您提供ByteBuffer数组作为媒体输入,但Android 4.3(SDK 18)现在允许您使用Surface作为编码器的输入。例如,这使您可以从现有视频文件中编码输入,或者使用由OpenGL ES生成的帧。

Media Muxer是在Android 4.3(SDK 18)中添加的,因此为了方便地使用Media Muxer编写mp4文件,您应该使用SDK>=18。 使用Media Codec API,您将获得硬件加速编码,并且可以轻松编码高达60 FPS。
您可以从以下三个选项中开始:1)如何使用MediaCodec将位图编码为视频? 或使用 2) Google Grafika3) Bigflake

从Grafika的RecordFBOActivity.java开始。用自己的位图替换Choreographer事件以进行编码,去除屏幕上的绘制,将位图作为Open GL纹理加载并在Media Codec输入表面上绘制。


你好,谢谢提供链接。您能帮忙编写完整的代码吗? - Khalil Khalaf
1
请查看以下代码:https://dev59.com/geo6XIcBkEYKwwoYKRLd - AndreyICE

10

jCodec已添加了对Android的支持。

您需要将这些添加到您的gradle中...

implementation 'org.jcodec:jcodec:0.2.3'
implementation 'org.jcodec:jcodec-android:0.2.3'

...并且

android {
    ...
    configurations.all {
        resolutionStrategy.force 'com.google.code.findbugs:jsr305:3.0.2'
    }
}

我可以确认这个工作是预期的,但有一些注意事项。首先,当我尝试使用一些全尺寸图像并写入文件时,会出现播放错误。当我缩小图像大小时,如果图像的宽度或高度不是偶数,则会出现错误,因为它需要YUV420J色彩空间的2的倍数。

另外值得注意的是,这会使您的包变得非常沉重。我的小项目通过添加这个功能超过了dex限制,并且需要启用multidex。

FileChannelWrapper out = null;
File dir = what ever directory you use...
File file = new File(dir, "test.mp4");

try { out = NIOUtils.writableFileChannel(file.getAbsolutePath());
      AndroidSequenceEncoder encoder = new AndroidSequenceEncoder(out, Rational.R(15, 1));
      for (Bitmap bitmap : bitmaps) {
          encoder.encodeImage(bitmap);
      }
      encoder.finish();
} finally {
    NIOUtils.closeQuietly(out);
}

我正在使用CameraX和ImageAnalysis.Analyzer同时分析图像,同时我想将用户所看到的内容存储到视频文件中,以便后续调试使用,这段代码确实帮了我很大忙。谢谢。 - JaydeepW
在我的情况下,当使用145张图片(每张图片200kb低质量)调用此函数时,需要很长时间,大约需要2分钟。有没有任何方法可以提高它的速度? - Ankush Shrivastava

5
您可以使用Bitmp4将图像序列转换为MP4文件。
示例代码:

...

val encoder = MP4Encoder()
     encoder.setFrameDelay(50)
     encoder.setOutputFilePath(exportedFile.path)
     encoder.setOutputSize(width, width)

 startExport()

 stopExport()

 addFrame(bitmap) //called intervally

这是一个Java库,因此很容易将其导入到Android项目中,不像ffmpeg那样需要使用NDK。请参考https://github.com/dbof10/Bitmp4获取示例代码和下载文件。

3

谢谢!我可以确认这个已经工作了 :) - Michael N

1

Abhishek V是正确的,关于的更多信息: 请参见从图像列表制作Android动画视频

最近我使用树莓派和Android设备构建了一个实时视频系统,遇到了与你相同的问题。我使用了一些实时流传输协议,如RTP / RTCP,而不是保存图像文件列表。如果您的要求类似于此,则可以更改策略。

另一个建议是您可以探索一些C / C ++库,使用NDK / JNI来突破Java的限制。

希望这些建议对您有所帮助 :)


是的,我理解了 :) 感谢您的建议和链接。我们可以去聊天室,也许我们可以更多地讨论策略? - Khalil Khalaf
这个页面上的内容对我帮助很大,那里的示例非常实用:android-streaming-live-camera-video-to-web。如果这种策略符合您的需求,并且您对实现方面有更多问题,我们可以继续交流。 - Tommy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接