前两章我们介绍了视频的
RGB、YUV原始格式,以及如何对摄像头数据进行采集,本章将介绍如何编码YUV数据
一、视频编码
在前一章,介绍了如何对摄像头数据进行采集,尝试过的朋友可能会发现,采集的NV21数据,几秒钟下来,文件就上百M了,我们平时看的MP4,几秒也才几M,这是怎么回事呢
这是因为我们存储的是原始格式,是未进行压缩的数据,所以存储下来是特别大
其实不光是存储大,一般原始格式的视频,也是无法播放的,因为播放无法知晓你的一大堆数据是干什么的,播放器一般得知道视频的帧率、分辨率、码率等一些参数,才能正确的解析视频并进行解码播放
所以接下来我们先来编码摄像头采集的数据
常见的编码格式有H26x,一般Mp4的视频编码格式是H264,这种编码格式能够大大的减少存储大小
回顾一下上一章的视频采集过程:
- 打开摄像头
- 初始化摄像头
- 设置预览回调
- 开始预览
- 在预览回调中,获取
NV21数据,并写入文件中 - 停止预览,释放相机
那么对于编码的话,需要在第5步进行修改,加入编码的逻辑,步骤变为:
- 打开摄像头
- 初始化摄像头
- 设置预览回调
- 开始预览
- 在预览回调中,获取
NV21数据,转换成NV12数据,传入编码器 - 在编码器输出中,获取编码后的数据,写入文件或者
MediaMuxer - 停止预览,释放相机
可以注意到,我们再预览回调中,获取的是NV21的数据,而在传入编码器时,需要的是NV12的数据(编码器对NV12格式的数据支持最好),所有中间有个转换的过程,关于这个转换,我们下面会有详细的讲解
还有一个是,在第6步中,我们有两个选择,如果需要使我们编码后的视频能够在任意播放器中播放出来,那么可以使用MediaMuxer将其封装成Mp4
那么,接下来我们就上述进行具体的实现
二、YUV旋转、镜像
2.1 YUV旋转
在预览回调中,我们获取到了原始的NV21数据,不过需要的是注意,此时的图像是被旋转过的,及时我们在初始化摄像头时对其角度进行了旋转,预览画面也显示正常,但预览回调还是被选中90或270度的,所有我们在将其转换成NV12时,还需注意旋转角度问题
旋转角度
我们在初始化摄像头的时候,设置了旋转角度
camera.setDisplayOrientation(orientation = CameraUtils.getDisplayOrientation(activity, facing));
复制代码
因为在NV21转NV12时需要用到,所以这里我们用一个变量保存起来
下面我们来进行各种角度的旋转
旋转90度
首先,假设图像是这样的

接着顺时针旋转90度,并转换成NV12的格式,可以得到

我们先来看Y的数据
NV12的第一行取的是NV21的第一列的数据,第二行取的是第二列的数据,依次类推,就得到了Y的数据
再来看UV的数据,因为NV12和NV21是YUV420sp格式的数据,所以需要将UV看做一个整体进行旋转
NV12的第一和第二个数据取的是U3和V3,本来存放顺序是V3->U3的,但因为NV12的UV顺序和NV21的UV顺序刚好相反,所以需要将二者倒过来,依次类推,就得到了上图的数据存储形式
接下来看代码如何实现
/**
* nv21转nv12,并且旋转90度
*/
private static byte[] nv21ToNv12AndRotate90(byte[] inputData, int width, int height) {
if (inputData == null) {
return null;
}
int size = inputData.length;
if (size != (width * height * 3 / 2)) {
return null;
}
byte[] outputData = new byte[size];
int k = 0;
for (int i = 0; i < width; i++) {
for (int j = height - 1; j >= 0; j--) {
outputData[k++] = inputData[width * j + i];
}
}
int start = width * height;
for (int i = 0; i < width; i += 2) {
for (int j = height / 2 - 1; j >= 0; j--) {
outputData[k++] = inputData[start + width * j + i + 1];
outputData[k++] = inputData[start + width * j + i];
}
}
return outputData;
}
复制代码
代码中,有两个for循环,第一个是处理Y数据,而第二个是处理UV数据
从上面的图示和代码中,我们不难发现旋转其他角度的规律
旋转180度
/**
* nv21转nv12,并且旋转180度
*/
private static byte[] nv21ToNv12AndRotate180(byte[] inputData, int width, int height) {
if (inputData == null) {
return null;
}
int size = inputData.length;
if (size != (width * height * 3 / 2)) {
return null;
}
byte[] outputData = new byte[size];
int k = 0;
for (int i = height - 1; i >= 0; i--) {
for (int j = width - 1; j >= 0; j--) {
outputData[k++] = inputData[width * i + j];
}
}
int start = width * height;
for (int i = height / 2 - 1; i >= 0; i--) {
for (int j = width - 1; j >= 0; j -= 2) {
outputData[k++] = inputData[start + width * i + j];
outputData[k++] = inputData[start + width * i + j - 1];
}
}
return outputData;
}
复制代码
旋转270度
/**
* nv21转nv12,并且旋转270度
*/
private static byte[] nv21ToNv12AndRotate270(byte[] inputData, int width, int height) {
if (inputData == null) {
return null;
}
int size = inputData.length;
if (size != (width * height * 3 / 2)) {
return null;
}
byte[] outputData = new byte[size];
int k = 0;
for (int i = width - 1; i >= 0; i--) {
for (int j = 0; j < height; j++) {
outputData[k++] = inputData[width * j + i];
}
}
int start = width * height;
for (int i = width - 1; i >= 0; i -= 2) {
for (int j = 0; j < height / 2; j++) {
outputData[k++] = inputData[start + width * j + i];
outputData[k++] = inputData[start + width * j + i - 1];
}
}
return outputData;
}
复制代码
以上,就包括了相机旋转的所有角度,不过,还有一点需要注意,一般,前摄的图像是相反的,也就是说,我们在处理前摄时,还需要对其进行镜像处理
2.2 YUV镜像
/**
* 镜像处理
*/
private static byte[] yuvMirror(byte[] inputData, int width, int height) {
if (inputData == null) {
return null;
}
int size = inputData.length;
byte[] outputData = new byte[size];
int k = 0;
for (int i = 0; i < height; i++) {
for (int j = width - 1; j >= 0; j--) {
outputData[k++] = inputData[width * i + j];
}
}
int start = width * height;
for (int i = 0; i < height / 2; i++) {
for (int j = width - 1; j >= 0; j -= 2) {
outputData[k++] = inputData[start + width * i + j - 1];
outputData[k++] = inputData[start + width * i + j];
}
}
return outputData;
}
复制代码
镜像处理比较简单,就是将每行的数据倒序存放,但需要注意UV是个整体,在处理的时候也不要忘记
我们再来做一个方法,根据传入的数据、尺寸、摄像头类型、旋转角度来做处理
/**
* nv21转换nv12
*/
public static byte[] cameraNv21ToNv12(
byte[] data,
int width,
int height,
int facing,
int orientation) {
byte[] outputData;
Log.d(TAG, "cameraNv21ToNv12: " + orientation + " facing:" + facing);
int rotate = orientation;
int w = width;
int h = height;
if (facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
rotate = 360 - orientation;
}
switch (rotate) {
case 90:
// 经过旋转,宽高互换
w = height;
h = width;
outputData = nv21ToNv12AndRotate90(data, width, height);
break;
case 180:
// 经过旋转,宽高不变
outputData = nv21ToNv12AndRotate180(data, width, height);
break;
case 270:
// 经过旋转,宽高互换
w = height;
h = width;
outputData = nv21ToNv12AndRotate270(data, width, height);
break;
default:
outputData = data;
break;
}
return cameraNv21ToNv12WidthFacing(outputData, w, h, facing);
}
/**
* 通过facing获取nv12数据
*/
private static byte[] cameraNv21ToNv12WidthFacing(
byte[] data,
int width,
int height,
int facing) {
if (facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
// 前摄是镜像的,所有需要做一次镜像处理
return yuvMirror(data, width, height);
}
return data;
}
复制代码
从上面可以看出,我们只暴露了一个cameraNv21ToNv12()方法给外部调用
三、MediaCodec编码
在学习了如何对YUV进行编码前预处理后,下面就开始真正的编码
初始化MediaCodec
private void initMediaCodec() {
int width = this.height;
int height = this.width;
try {
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_BIT_RATE,
width * height * 4);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
//设置压缩等级 默认是baseline
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileMain);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel3);
}
}
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
mediaCodec.setCallback(this);
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
} catch (IOException e) {
e.printStackTrace();
}
}
复制代码
对于MediaCodec的使用,我们在前面音频编码已经有一定了解,不过之前我们都是使用的MediaCodec的同步编码方式,下面我们将使用MediaCodec的异步编码方式
MediaCodec在setCallback后,需要实现几个回调方法
-
onInputBufferAvailable()在
输入buffer可用时回调,此时可以在此插入数据 -
onOutputBufferAvailable()在编码数据完成时回调,此时的数据是已经编码的
H264数据,可以将此数据写入MediaMuxer中 -
onError()发生错误时返回
-
onOutputFormatChanged()在此处,可以对输出的
MediaFormat处理,比如调用MediaMuxer.addTrack方法得到一个TrackId,用于写入MediaMuxer
在初始化MediaCodec后,我们可以对MediaMuxer进行初始化,MediaMuxer可以帮助我们生成Mp4,方便我们查看视频是否编码正常
private void initMediaMuxer(String path) {
if (TextUtils.isEmpty(path)) {
return;
}
File file = new File(path);
if (file.exists()) {
file.delete();
}
try {
mediaMuxer = new MediaMuxer(path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
e.printStackTrace();
mediaMuxer = null;
}
}
复制代码
从上面的描述来看,我们需要将摄像头和编码的操作放入子线程,而摄像头的预览数据回调和编码插入取出数据又都是异步的,那么我们应该怎么办呢
其实我们可以使用一个队列,存放预览回调的数据,然后在编码的时候,将队列的数据取出插入编码器,这样就可以达到想要的效果
对于队列,我们可以使用LinkedBlockingQueue,它是一个线程安全的队列
private final BlockingQueue<byte[]> queue = new LinkedBlockingQueue<>(10);
复制代码
预览数据处理
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (this.data == null) {
this.data = new byte[width * height * 3 / 2];
}
camera.addCallbackBuffer(this.data);
queue.offer(YuvUtils.cameraNv21ToNv12(this.data, width, height, facing, orientation));
}
复制代码
在预览回调中,我们调用了之前所介绍的方法cameraNv21ToNv12将nv21数据转换成nv12数据,并将其传入队列中
插入编码器
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
ByteBuffer buffer = codec.getInputBuffer(index);
buffer.clear();
int size = 0;
byte[] data = queue.poll();
if (data != null) {
buffer.put(data);
size = data.length;
}
codec.queueInputBuffer(index, 0, size, System.nanoTime() / 1000, 0);
}
复制代码
在onInputBufferAvailable()回调中,我们取出数据,并插入编码器中,注意,插入时需要传入时间戳,我们传入System.nanoTime() / 1000即可
添加视频轨道
在写入MediaMuxer前,我们还得先将视频的输出格式写入MediaMuxer,这样才能回去一个TrackId供我们写入数据
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
if (trackIndex != -1) {
return;
}
if (mediaMuxer == null) {
return;
}
trackIndex = mediaMuxer.addTrack(format);
mediaMuxer.start();
}
复制代码
获取编码数据,写入MediaMuxer
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
ByteBuffer buffer = codec.getOutputBuffer(index);
if (buffer != null && info.size > 0) {
if (mediaMuxer != null && trackIndex != -1) {
mediaMuxer.writeSampleData(trackIndex, buffer, info);
}
buffer.clear();
}
codec.releaseOutputBuffer(index, false);
}
复制代码
这样,我们就完成了整个视频采集到编码,最后到输出Mp4的过程
最后,别忘了释放相关资源
private void closeCamera() {
if (camera == null) {
return;
}
camera.stopPreview();
camera.release();
camera = null;
}
private void stopMediaMuxer() {
if (mediaMuxer == null) {
return;
}
mediaMuxer.stop();
mediaMuxer.release();
mediaMuxer = null;
}
private void stopMediaCodec() {
if (mediaCodec == null) {
return;
}
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
}
复制代码



















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)


![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)