Android用MediaCodec将相机预览帧编码成MP4视频


最近项目中,有一个在扫码同时录视频的需求。扫码框架是通过摄像头 onPreviewFrame方法获取预览帧数据然后解码二维码,要在不改变扫码的整体框架条件下完成录视频,自然想到了将每一帧预览图像依次编码成视频的做法(不涉及音频)。一通开发搞下来,感觉还是有很多值得学习记录的地方,遂有这篇博客。

知识预备

  • 首先这是一个典型的Camera设备应用场景,对相机的相关API要有一定了解,可以参考zxing扫码项目。
  • 有视频功能开发,对视频编解码、图像数据格式,要有一些了解。部分参考:
    • 比特率 bps (Bit Per Second)
      比特率越高,每秒传送数据就越多,画质就越清晰,视频文件占用空间也越大。更多参考 比特率-百度百科比特率 Wiki

    • 帧率 fps (Frame Per Second)
      视频每秒传输的帧数(画面数),每秒帧数越多,显示的画面就越流畅,但对显卡(GPU)的要求也越高。更多参考 fps-百度百科

    • YUV 维基百科 / 百度百科 / VideoLAN Wiki
      YUV,是一种颜色编码方法。Y’UV, YUV, YCbCr, YPbPr 等专有名词都可以称为YUV,彼此有重叠。
      Y - 明亮度(Luminance、Luma)
      U(Cb)、V(Cr) - 色度(Chrominance或Chroma),描述影像色彩及饱和度 ,即像素的颜色。

      YUV Formats分成两个格式:
      紧缩格式(packed formats):将Y、U、V值存储成Macro Pixels数组,和RGB的存放方式类似。
      平面格式(planar formats):将Y、U、V的三个分量分别存放在不同的矩阵中。

    • 图像格式
      一些参考:
      【图像】数据格式介绍(yuv420sp、yuv420sp、yv12,nv12等)
      NV12与YV12,YUV的主要格式
      图解YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P、YUV444P的区别
      为节省带宽起见,大多数YUV格式平均使用的每像素位数都少于24位。主要的采样格式有YCbCr 4:2:0、YCbCr 4:2:2、YCbCr 4:1:1和YCbCr 4:4:4。YUV的表示法称为A:B:C表示法:

      YUV 4:4:4采样,每一个Y对应一组UV分量,一个YUV占8+8+8 = 24bits 3个字节。
      YUV 4:2:2采样,每两个Y共用一组UV分量,一个YUV占8+4+4 = 16bits 2个字节。
      YUV 4:2:0采样,每四个Y共用一组UV分量,一个YUV占8+2+2 = 12bits 1.5个字节。
      我们最常见的YUV420P和YUV420SP都是基于4:2:0采样的,所以如果图片的宽为width,高为heigth,在内存中占的空间为width * height * 3 / 2,其中前width * height的空间存放Y分量,接着width * height * 1 / 4存放U分量,最后width * height * 1 / 4存放V分量。
      ————————————————
      原文链接:https://blog.csdn.net/byhook/article/details/84037338

      • YUV444
      • YUV422
      • YUV420
        YUV420根据U、V相同分量是否连续排列,分为YUV420P和YUV420SP。U和V分别连续存储的,是YUV420P;U和V交叉存储的,是YUV420SP。
        • YUV420P
          YUV420P是多平面模式,Y , U , V分别在不同平面,有三个平面。根据Y后面UV的先后顺序,又可以分为YU12和YV12。YU12也叫I420
          • YU12 (I420):YYYYYYYY UU VV
          • YV12:YYYYYYYY VV UU
        • YUV420SP
          SP(Semi-planar),指YUV不是分成3个平面而是分成2个平面。Y数据一个平面,UV数据合用一个平面。根据UV的数据排列先后顺序,又分成NV12和NV21:
          • NV12:YYYYYYYY UV UV
          • NV21:YYYYYYYY VU VU

实现思路

在Android上,要将连续的多张图片编码成视频,主要有两种做法:

  1. FFmpeg,软编、解码
  2. MediaCodec,提供对Android底层的媒体编解码组件访问,可以使用硬件编、解码。
    MediaCodec编码的输出是H264的码流,如果需要保存为MP4文件,还需要使用MediaMuxer进行保存

本文记录的是使用MediaCodec+MediaMuxer的实现方法。

获取图像数据帧

通过camera.setPreviewCallback()方法设置预览帧回调,设置后在相机预览期间,onPreviewFrame方法会连续被回调,经过测试,每秒大约会被调用16次,也就是每秒我们可以获取到大约16帧图像数据。

	/**
     * Called as preview frames are displayed.  This callback is invoked
     * on the event thread open(int) was called from.
     * If using the ImageFormat.YV12 format,
     * refer to the equations in Camera.Parameters.setPreviewFormat
     * for the arrangement of the pixel data in the preview callback
     * buffers.
     *
     * @param data - the contents of the preview frame in the format defined
     *  by ImageFormat, which can be queried with Camera.Parameters.getPreviewFormat.
     *  If Camera.Parameters.setPreviewFormat is never called, 
     *  the default will be the YCbCr_420_SP (NV21) format.
     * @param camera - the Camera service object.
     */
	@Override
    public void onPreviewFrame(byte[] data, Camera camera) {
    	// 预览帧图像处理
    }

通过这个方法的注释可以知道,如果没有设置过预览图像的格式,那么onPreviewFrame返回的数据格式默认是NV21
在初始化MediaCodec的时候,目前编码器对YUV420格式,推荐使用的只有COLOR_FormatYUV420Flexible这一个,其他的都被标为deprecated:
在这里插入图片描述
它的文档解释是这样的:

/**
 * Flexible 12 bits per pixel, subsampled YUV color format with 8-bit chroma and luma components.
 * Chroma planes are subsampled by 2 both horizontally and vertically. Use this format with Image.
 * This format corresponds to YUV_420_888, and can represent the COLOR_FormatYUV411Planar, 
 * COLOR_FormatYUV411PackedPlanar, COLOR_FormatYUV420Planar, COLOR_FormatYUV420PackedPlanar, 
 * COLOR_FormatYUV420SemiPlanar and COLOR_FormatYUV420PackedSemiPlanar formats.
 */
public static final int COLOR_FormatYUV420Flexible            = 0x7F420888;

COLOR_FormatYUV420Flexible 这个格式对应YUV_420_888,是一种通用格式,可以描述任意一种YUV420平面或半平面格式。

这里还有个一直困扰我的疑问:
参考其他文章的时候,都有一步将NV21转为NV12的操作,再传入MediaCodeC进行编码的,否则输出的视频颜色有问题,是黑白的。我尝试了,确实需要这么个转换,但是没有找到有确切解释的文档资料。所以目前只能是照做,原因待查。

编码视频

初始化编码器

	MediaFormat mediaFormat;
	mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, size.width, size.height);
	// YUV 420 对应的是图片颜色采样格式
	mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities
	    .COLOR_FormatYUV420Flexible);
	
	// 1Mbps=128KB/s * 8 = 1048576
	mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1048576);
	// 帧率,eg:25
	mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, RAME_RATE);
	// I 帧间隔
	mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
	try {
		mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
		String mp4Path = FileUtils.getFilesDir() + File.separator + TMP_NAME + ".mp4";
		// 创建混合器生成MP4
		mediaMuxer = new MediaMuxer(mp4Path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
		//进入配置状态
		mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
		//进行生命周期执行状态
		mediaCodec.start();
	} catch (IOException e) {
		e.printStackTrace();
	}

编码转换

public static void NV21ToNV12(byte[] nv21, byte[] nv12, int width, int height) {
    if (nv21 == null || nv12 == null) {
        return;
    }
    int framesize = width * height;
    // NV21: YYYYYYYY VUVU
    // NV12: YYYYYYYY UVUV 
    System.arraycopy(nv21, 0, nv12, 0, framesize);
    for (int i = 0; i < framesize / 2; i += 2) {
    	// U
        nv12[framesize + i * 2] = nv21[framesize + i + 1];
        // V
        nv12[framesize + i * 2 + 1] = nv21[framesize + i];
    }
}

编码视频

@Override
public void run() {
    super.run();
    while (true) {
        try {
            // 拿到有空闲的输入缓存区下标
            int inputBufferId = mediaCodec.dequeueInputBuffer(-1);
            if (inputBufferId >= 0) {
                // 从相机预览帧缓冲队列中取出一帧待处理的数据,需要NV21->NV12的转换
                byte[] tempByte = getQueuedBuffer();
                //有效的空的缓存区
                ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
                if (tempByte == null) {
                    break;
                }
                inputBuffer.put(tempByte);
                frameIndex++;
                // 微秒时间戳
                long presentationTime = frameIndex * FRAME_INTERVAL_MS * 1000;
                //将数据放到编码队列
                mediaCodec.queueInputBuffer(inputBufferId, 0, tempByte.length, presentationTime, 0);
            }
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            //得到成功编码后输出的out buffer Id
            int outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
            if (outputBufferId >= 0) {
                ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
                // mediacodec的直接编码输出是h264
                byte[] h264= new byte[bufferInfo.size];
                outputBuffer.get(h264);
                outputBuffer.position(bufferInfo.offset);
                outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                // 将编码后的数据写入到MP4复用器
                mediaMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo);
                //释放output buffer
                mediaCodec.releaseOutputBuffer(outputBufferId, false);
            } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                MediaFormat mediaFormat = mediaCodec.getOutputFormat();
                mTrackIndex = mediaMuxer.addTrack(mediaFormat);
                mediaMuxer.start();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    mediaCodec.stop();
    mediaCodec.release();
    mediaCodec = null;

    mediaMuxer.stop();
    mediaMuxer.release();
    mediaMuxer = null;
    saveVideoLatch.countDown();
}

问题记录

  1. 生成的视频花屏
  2. 播放速度不正常,或快或慢
    调用 mediaCodec.queueInputBuffer 时的presentationTimeUs参数要按照帧序列传正确
  3. 视频马赛克严重
    适当提高视频分辨率或给mediaFormat设置的比特率
  4. 竖屏应用时,视频方向旋转90°
    • 先将从 onPreviewFrame 获取的图像数据旋转90°。
    • 谈谈关于Android视频编码的那些坑这篇文章还提到了一种在mp4文件格式的头部指定一个旋转矩阵的方案,这样应该是更高效的,笔者暂未尝试。
      旋转处理参考:
	/** 顺时针旋转90° */
    public static byte[] rotateYUV420Degree90(byte[] data, int imageWidth, int imageHeight) {
        byte[] yuv = new byte[imageWidth * imageHeight * 3 / 2];
        int i = 0;
        for (int x = 0; x < imageWidth; x++) {
            for (int y = imageHeight - 1; y >= 0; y--) {
                yuv[i] = data[y * imageWidth + x];
                i++;
            }
        }
        i = imageWidth * imageHeight * 3 / 2 - 1;
        for (int x = imageWidth - 1; x > 0; x = x - 2) {
            for (int y = 0; y < imageHeight / 2; y++) {
                yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth) + x];
                i--;
                yuv[i] = data[(imageWidth * imageHeight) + (y * imageWidth)
                        + (x - 1)];
                i--;
            }
        }
        return yuv;
    }
  1. 部分手机上生成的视频是黑白的。
以下是一个简单的例子,演示如何使用MediaCodec将YUV格式的视频编码MP4格式: ```cpp #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/mman.h> #include <media/NdkMediaCodec.h> #include <media/NdkMediaFormat.h> #define MAX_BUFFER_SIZE 1024 * 1024 int main(int argc, char *argv[]) { if (argc != 4) { printf("Usage: %s <input.yuv> <output.mp4> <fps>\n", argv[0]); return 0; } const char *input_file = argv[1]; const char *output_file = argv[2]; const int fps = atoi(argv[3]); // 打开输入文件 int input_fd = open(input_file, O_RDONLY); if (input_fd < 0) { printf("Failed to open input file: %s\n", input_file); return -1; } // 获取输入文件大小 struct stat input_stat; if (fstat(input_fd, &input_stat) < 0) { printf("Failed to get input file size.\n"); close(input_fd); return -1; } // 映射输入文件 uint8_t *input_data = (uint8_t *)mmap(NULL, input_stat.st_size, PROT_READ, MAP_PRIVATE, input_fd, 0); if (input_data == MAP_FAILED) { printf("Failed to mmap input file.\n"); close(input_fd); return -1; } // 初始化输出文件 int output_fd = open(output_file, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (output_fd < 0) { printf("Failed to open output file: %s\n", output_file); munmap(input_data, input_stat.st_size); close(input_fd); return -1; } // 初始化编码器 AMediaCodec *codec = AMediaCodec_createEncoderByType("video/avc"); AMediaFormat *format = AMediaFormat_new(); AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/avc"); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, 1920); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, 1080); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, fps); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, OMX_COLOR_FormatYUV420SemiPlanar); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, 8000000); AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, 1); AMediaCodec_configure(codec, format, NULL, NULL, AMEDIACODEC_CONFIGURE_FLAG_ENCODE); AMediaCodec_start(codec); // 编码循环 uint8_t *input_buffer = NULL; uint8_t *output_buffer = NULL; size_t input_buffer_size = 0; size_t output_buffer_size = 0; AMediaCodecBufferInfo buffer_info; ssize_t input_buffer_index = -1; ssize_t output_buffer_index = -1; size_t frame_size = input_stat.st_size / (fps * 10); // 每大小 size_t frame_index = 0; size_t pos = 0; while (true) { // 获取输入缓冲区 input_buffer_index = AMediaCodec_dequeueInputBuffer(codec, 2000); if (input_buffer_index >= 0) { input_buffer = AMediaCodec_getInputBuffer(codec, input_buffer_index, &input_buffer_size); if (input_buffer_size >= frame_size) { memcpy(input_buffer, input_data + pos, frame_size); AMediaCodec_queueInputBuffer(codec, input_buffer_index, 0, frame_size, frame_index * 1000000 / fps, 0); pos += frame_size; frame_index++; } } // 获取输出缓冲区 output_buffer_index = AMediaCodec_dequeueOutputBuffer(codec, &buffer_info, 2000); if (output_buffer_index >= 0) { output_buffer = AMediaCodec_getOutputBuffer(codec, output_buffer_index, &output_buffer_size); write(output_fd, output_buffer, buffer_info.size); AMediaCodec_releaseOutputBuffer(codec, output_buffer_index, false); } // 结束条件 if (pos >= input_stat.st_size) { break; } } // 停止编码器 AMediaCodec_stop(codec); AMediaCodec_delete(codec); AMediaFormat_delete(format); // 关闭文件 munmap(input_data, input_stat.st_size); close(input_fd); close(output_fd); return 0; } ``` 在这个例子中,我们使用了以下的步骤: 1. 打开输入文件并获取文件大小。 2. 将输入文件映射到内存中。 3. 初始化输出文件。 4. 初始化编码器,并设置编码器的格式。 5. 进行编码循环,将YUV数据送给编码器。 6. 将编码输出写入到输出文件中。 7. 停止编码器并关闭文件。 需要注意的是,这个例子中的编码器设置了固定的宽度、高度、率、颜色格式、比特率和关键间隔。在实际应用中,这些参数可能需要根据具体的视频进行调整。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值