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
- YUV420P
实现思路
在Android上,要将连续的多张图片编码成视频,主要有两种做法:
- FFmpeg,软编、解码
- 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.*/@Overridepublic 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 = 1048576mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1048576);// 帧率,eg:25mediaFormat.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";// 创建混合器生成MP4mediaMuxer = 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) {// Unv12[framesize + i * 2] = nv21[framesize + i + 1];// Vnv12[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 Idint outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);if (outputBufferId >= 0) {ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);// mediacodec的直接编码输出是h264byte[] 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 buffermediaCodec.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();
}
问题记录
- 生成的视频花屏
- 播放速度不正常,或快或慢
调用mediaCodec.queueInputBuffer
时的presentationTimeUs参数要按照帧序列传正确 - 视频马赛克严重
适当提高视频分辨率或给mediaFormat设置的比特率 - 竖屏应用时,视频方向旋转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;}
- 部分手机上生成的视频是黑白的。
Android用MediaCodec将相机预览帧编码成MP4视频相关推荐
- Android stdio 实时获取相机预览图像(详细)
activity_main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout ...
- Android 相机预览帧存入本地
fun decodeToBitMap(data: ByteArray?, frame: Frame): Bitmap? {try {//格式成YUV格式val yuvimage = YuvImage( ...
- Android相机预览设置适配及显示方式
Android相机的部分工作原理. 预览流程 相机预览是Android Camera最常用的功能之一,它是很多功能重要的输入,例如扫码.AR等. 一般而言,相机预览的整体流程,可以通过下图表示: 其中 ...
- 2022-07-21 Android 相机预览数据转Bitmap优化
背景 在Android开发中我们有时候会需要拿到相机预览数据转成bitmap自己做处理. 常用方式 在Android API中提供了 一套转换方式: private Bitmap convertBit ...
- Android AR开发实践之七:OpenGLES相机预览背景绘制源码详解
Android AR开发实践之七:OpenGLES相机预览背景绘制源码详解 目录 Android AR开发实践之七:OpenGLES相机预览背景绘制源码详解 一.OpenGL ES渲染管线 1.基本处 ...
- Android Camera API/Camera2 API 相机预览及滤镜、贴纸等处理
Android Lollipop 增加了Camera2 API,并将原来的Camera API标记为废弃了.相对原来的Camera API来说,Camera2是重新定义的相机 API,也重构了相机 A ...
- android 圆形相机预览拍照_Android多种方式实现相机圆形预览
最终效果图如下: 一.为预览控件设置圆角 public RoundTextureView(Context context, AttributeSet attrs) { super(context, a ...
- android 圆形相机预览拍照_Android多种方式实现相机圆形预览的示例代码
效果图如下: 一.为预览控件设置圆角 public RoundTextureView(Context context, AttributeSet attrs) { super(context, att ...
- Android Camera2 相机预览、获取数据
Camera2简要说明 在Google 推出Android 5.0的时候, Android Camera API 版本升级到了API2(android.hardware.camera2),大幅提高了A ...
最新文章
- 无需服务器的个人博客 (2018.5.22更新)
- Ubuntu14.04安装和配置Tomcat8.0.12(转)
- c语言考试题及答案 大一,大一C语言期末考试试题
- [Redis6]常用数据类型_List列表
- 创建存储过程向表中循环加入数据
- MNIST的AlexNet实现
- 王家林Spark 课程,蘑菇云,IMF真相
- Science Advances:社会和健康科学中用于描述、预测和因果推理的机器学习方法
- java jws配置_与WordNet有关的两个JAVA库(JWNL和JWS)的配置
- php session 自定义到数据库,PHP 自定义session储存 数据库 方式类 高洛峰 细说PHP...
- arm push/pop/b/bl汇编指令
- TIM基本定时器——定时
- 比例运算电路小结(电路组态 输入电阻)
- 移动端前台页面需要注意的几点
- 前端实习一个月的收获与思考
- 计算机学院社会实践,计算机学院、软件学院暑期社会实践
- Cisco三层交换机的配置详解
- 十多年后人类将登陆火星,这项任务总共分几步
- 【Python绘图】绘图常见的构成元素1
- 一定有你需要收藏的网站!
热门文章
- Linux 内存管理。
- 北师大1903计算机在线答案,[南开大学(本部)]20秋学期(1709、1803、1809、1903、1909、2003、2009 )《程序设计基础(下)》在线作业-2...
- IT业狼多肉少?程序员的绝地求生,谁能给他们一把“98K”?
- 抖音好物分享怎么做?
- 【项目管理软件盛宴】谁才是人气王?
- Android成长之路-音乐播放器的实现
- 热爱生活的人请过来看看:有没有通过叶子或花来识别植物的软件?
- g第十四周,十五周作业
- Wattagio for Mac(电池管理)
- mountain乐谱-蒲公英的约定