onPreviewFrame
方法获取预览帧数据然后解码二维码,要在不改变扫码的整体框架条件下完成录视频,自然想到了将每一帧预览图像依次编码成视频的做法(不涉及音频)。一通开发搞下来,感觉还是有很多值得学习记录的地方,遂有这篇博客。
比特率 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
在Android上,要将连续的多张图片编码成视频,主要有两种做法:
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;
int i, j;
System.arraycopy(nv21, 0, nv12, 0, framesize);
for (i = 0; i < framesize; i++) {
nv12[i] = nv21[i];
}
for (j = 0; j < framesize / 2; j += 2) {
nv12[framesize + j - 1] = nv21[j + framesize];
}
for (j = 0; j < framesize / 2; j += 2) {
nv12[framesize + j] = nv21[j + framesize - 1];
}
}
@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();
}
mediaCodec.queueInputBuffer
时的presentationTimeUs参数要按照帧序列传正确onPreviewFrame
获取的图像数据旋转90°。 /** 顺时针旋转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;
}