CameraX采集数据生成 YUV_420_888格式
通过分析接口得到ImageProxy 然后得到planes数组
@Override
public void analyze(ImageProxy image, int rotationDegrees) {
int width = image.getWidth();
int height = image.getHeight();
//格式 YUV/RGB...
int format = image.getFormat();
//图像数据
ImageProxy.PlaneProxy[] planes = image.getPlanes();
//数组3个元素 Y U V
}
I420的排列 YYYY YYYY YYYY YYYY UUUU VVVV
对于U和V数据 排列方式可能有两种
1. planes[1] = {uuuu...} planes[2] = {vvvv...}
2. planes[1] = {uvuv...} planes[2] = {vuvu...}
通过 int pixelstride = plane.getPixelStride()
获取返回值 0 表示上述第一种情况 返回值1 表示上述第二种情况
YUV数据获取 需要考虑RowStride 步长问题(字节对齐)
Y数据
1) 若RowStride = width 直接通过planes[0].getBuffer获取Y数据
2) 若RowStride > width 如4*4的I420每行以8字节对齐 还需要考虑最后一行无占位数据
//用于保存获取的I420数据。大小为:y+u+v, width*height + width/2*height/2 + width/2*height/2 ByteBuffer yuv420 =
ByteBuffer.allocate(image.getWidth() * image.getHeight() * 3 / 2);
int rowStride = plane.getRowStride();
ByteBuffer buffer = plane.getBuffer();
byte[] row = new byte[image.getWidth()];
// 每行要排除的无效数据,但是需要注意:实际测试中 最后一行没有这个补位数据
// 因为Y数据 RowStride 为大于等于Width,所以不会出现负数导致错误
// RowStride 等于Width,则得到空数组,不丢弃数据
byte[] skipRow = new byte[rowStride - image.getWidth()];
for (int i = 0; i < image.getHeight(); i++) {
buffer.get(row);
yuv420.put(row);
// 不是最后一行,则丢弃此数据
if (i < image.getHeight() - 1) {
buffer.get(skipRow);
}
}
U与V数据
1) = width
2) > width
3) = width/2
4) > width/2
1)planes[1]中不仅包含U数据,还会包含V的数据,此时pixelStride==2
planes[1]
planes[2]
2)Y数据一样,可能由于字节对齐,出现RowStride大于Width的情况,与等于Width一样, planes[1]中不仅包含U 数据,还会包含V的数据,此时pixelStride==2
planes[1]
planes[2]
3)获取的U数据对应的RowStride等于Width/2,表示我们得到的planes[1]只包含U数据。此时pixelStride==1
4)planes[1]只包含U数据,但是与Y数据一样,可能存在占位数据。此时pixelStride==1
获得了摄像头采集的数据之后,我们需要获取对应的YUV数据,需要根据pixelStride判断格式,同时还需要通过 rowStride来确定是否存在无效数据,那么最终我们获取YUV数据的完整实现为
//图像格式
int format = image.getFormat();
if (format != ImageFormat.YUV_420_888) {
//抛出异常
}
ByteBuffer i420 = ByteBuffer.allocate(image.getWidth() * image.getHeight() * 3 / 2);
// 3个元素 0:Y,1:U,2:V
ImageProxy.PlaneProxy[] planes = image.getPlanes();
// byte[]
/**
* Y数据
*/
//y数据的这个值只能是:1
int pixelStride = planes[0].getPixelStride();
ByteBuffer yBuffer = planes[0].getBuffer();
int rowStride = planes[0].getRowStride();
//1、rowStride 等于Width ,那么就是一个空数组
//2、rowStride 大于Width ,那么就是每行多出来的数据大小个byte
byte[] skipRow = new byte[rowStride - image.getWidth()];
byte[] row = new byte[image.getWidth()];
for (int i = 0; i < image.getHeight(); i++) {
yBuffer.get(row);
i420.put(row);
// 不是最后一行才有无效占位数据,最后一行因为后面跟着U 数据,没有无效占位数据,不需要丢弃
if (i < image.getHeight() - 1) {
yBuffer.get(skipRow);
}
}
/**
* U、V
*/
for (int i = 1; i < 3; i++) {
ImageProxy.PlaneProxy plane = planes[i];
pixelStride = plane.getPixelStride();
rowStride = plane.getRowStride();
ByteBuffer buffer = plane.getBuffer();
//每次处理一行数据
int uvWidth = image.getWidth() / 2;
int uvHeight = image.getHeight() / 2;
// 一次处理一个字节
for (int j = 0; j < uvHeight; j++) {
for (int k = 0; k < rowStride; k++) {
//最后一行
if (j == uvHeight - 1) {
//uv没混合在一起
if (pixelStride == 1) {
//rowStride :大于等于Width/2
// 结合外面的if:
// 如果是最后一行,我们就不管结尾的占位数据了
if (k >= uvWidth) {
break;
}
} else if (pixelStride == 2) {
//uv混在了一起
// rowStride:大于等于 Width
if (k >= image.getWidth()) {
break;
}
}
}
byte b = buffer.get();
// uv没有混合在一起
if (pixelStride == 1) {
if (k < uvWidth) {
i420.put(b);
}
} else if (pixelStride == 2) {
// uv混合在一起了
//1、偶数位下标的数据是我们本次要获得的U/V数据
//2、占位无效数据要丢弃,不保存
if (k < image.getWidth() && k % 2 == 0) {
i420.put(b);
}
}
}
}
}
//I420
byte[] result = i420.array();
旋转和缩放 可以使用openCV 或者 libyuv实现
视频编码和推流 使用h264
x264_encoder_encode(...);//编码的i_pts每次需要增长
H.264码流在网络中传输时实际是以NALU的形式进行传输的, 每个NAL之间由00 00 00 01 或者 00 00 01 进行分割
在分割符之后的第一个字节,就是表示这个nal的类型
- 0x67:sps
- 0x68: pps
- 0x65: IDR
for (int i = 0; i < pi_nal; ++i) {
int type = pp_nal[i].i_type;
uint8_t *p_payload = pp_nal[i].p_payload;//数据
int i_payload = pp_nal[i].i_payload;//数据长度
if(type == NAL_SPS){
spslen = i_payload - 4;//去掉间隔 00 00 00 01
sps=(uint8_t)alloca(spslen);//在栈中申请内存使用完自动释放
memcpy(sps,p_payload+4,spslen);
}else if(type == NAL_PPS){
ppslen = i_payload - 4;
pps=(uint8_t)alloca(ppslen);
memcpy(pps,p_payload+4,ppslen);
//发送数据 sendSPSPPS(sps, spslen, pps, ppslen);
}else{
//发送h264 sendH264(type, p_payload, i_payload);
}
}
音频编码和推流 faac
//输入样本 要给编码器编码的样本数
unsigned long inputSample;
unsigned long maxOutputBytes;
codec = faacEncOpen(sampleRate,channels,&inputSample,&maxOutputBytes);
inputByteNum = inputSample*2; //样本是16位 一个样本是2字节
//得到编码的各种参数配置
faacEncConfigurationPtr configurationPtr = faacEncGetCurrentConfiguration(codec);
configurationPtr->mpegVersion = MPEG4;
configurationPtr->aacObjectType=LOW;
configurationPtr->outputFormat=0;//传入0表示编码出aac裸数据 传1表示每一帧音频编码结果数据都会携带ADTS(包含采样率 声道等信息的数据头)
configurationPtr->inputFormat=FAAC_INPUT_16BIT;
faacEncSetConfiguration(codec,configurationPtr);