音视频开发路线:
Android 音视频开发入门指南_Jhuster的专栏的技术博客_51CTO博客_android 音视频开发入门
demo地址:
GitHub - wygsqsj/videoPath: 音视频学习路线demo
安卓摄像头采集得数据格式是NV21,NV21格式比较奇葩,安卓的MediaCodec编码也不支持,需要转换成NV12格式即YUVI420,他们两个的YUV排列顺序就是将UV调换了过来,NV21奇数位为V,偶数为U,NV12正好反过来:
NV21:
NV12:
无论哪种格式,都是一样的存储大小,一帧画面的大小计算公式,Y、U、V 每个都占一个byte,
也就是每一帧数据的存储大小就是 :宽* 高*3/2
当前就是普通的通过Camera1采集数据,并为Camera相机设置SurfaceView的预览:
mCamera = Camera.open(0);
if (mCamera == null) {
throw new RuntimeException("摄像机打开失败!");
}
try {
//设置camera数据回调
mCamera.setPreviewCallback(this);
mCamera.setDisplayOrientation(90);
if (parameters == null) {
parameters = mCamera.getParameters();
}
//获取默认的camera配置
parameters = mCamera.getParameters();
//设置预览格式
parameters.setPreviewFormat(ImageFormat.NV21);
//配置camera参数
mCamera.setParameters(parameters);
//将完全初始化的SurfaceHolder传入到setPreviewDisplay(SurfaceHolder)中
//没有surface的话,相机不会开启preview预览
mCamera.setPreviewDisplay(surfaceview.getHolder());
//NV21一帧的大小,其实就是 宽*高*3/2
callbackBuffer = new byte[width * height * 3 / 2];
mCamera.addCallbackBuffer(callbackBuffer);
mCamera.setPreviewCallback(this);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
从SurfaceView的回调中获取到yuv数据,放到一个队列中,供我们的编码线程获取:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo6);
surfaceview = findViewById(R.id.demo6Surface);
surfaceview.getHolder().addCallback(this);
}
//摄像头获取到的yuv数据回调
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//将当前帧图像保存在队列中
putYUVData(data, data.length);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
initBackCamera();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (null != mCamera) {
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
if (encodeThread != null) {
encodeThread.stopEncode();
}
if (encodeMuxerThread != null) {
encodeMuxerThread.stopEncode();
}
}
public void putYUVData(byte[] buffer, int length) {
// Log.i(LOG_TAG, "获取到摄像头数据" + length);
if (YUVQueue.size() >= 10) {
YUVQueue.poll();
}
YUVQueue.add(buffer);
}
摄像头获取摄像头数据是以横屏时的左上角为原点获取的,也就是我们摄像头捕捉的原始数据时横屏的,这也是我们为什么在初始化Camera时要把预览画面旋转90度的原因,先来看看未旋转时的图像:
而设置旋转90度后的图像:
未旋转时即便我们时竖屏下拍照,摄像头仍旧是捕捉的横屏画面,所以得到的也是横屏的照片,这时候我们按顺时针旋转90度即可得到竖屏的数据:
横屏转竖屏就是将YUV数据顺时针旋转90度即可得到:
/**
* 旋转yuv数据,将横屏数据转换为竖屏
*/
private byte[] revolveYuv(byte[] yuvData) {
byte[] revolveData = new byte[yuvData.length];
int y_size = width * height;
//uv高度
int uv_height = height >> 1;
//旋转y,左上角跑到右上角,左下角跑到左上角,从左下角开始遍历
int k = 0;
for (int i = 0; i < width; i++) {
for (int j = height - 1; j > -1; j--) {
revolveData[k++] = yuvData[width * j + i];
}
}
//旋转uv
for (int i = 0; i < width; i += 2) {
for (int j = uv_height - 1; j > -1; j--) {
revolveData[k++] = yuvData[y_size + width * j + i];
revolveData[k++] = yuvData[y_size + width * j + i + 1];
}
}
return revolveData;
}
通过FileOutStream来将编码好的数据写入到h264文件中,此中方式写出来来的264文件播放时没有时间戳标识,而且文件比较大,此处应该还有没有照顾到的点,但是可以写出264文件且进行播放代码如下:
1.配置MediaFormat,构建编码MediaCodec,注意我们配置宽高的时候,要按竖屏的宽高来配置
//构建对应的MeidaFormat,后期我们会将摄像头数据旋转90读成为竖屏,所以此处调换一下宽高
MediaFormat mediaFormat = MediaFormat.createVideoFormat(encodeMine, height, width);
//设置yuv格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
//比特率
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
//描述视频格式的帧速率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
//关键帧之间的间隔,此处指定为1秒
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
//构建编码h264MediaCodec
encodeCodec = MediaCodec.createEncoderByType(encodeMine);
encodeCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解码得到的byte[]数据的相关信息
//启动编码器
encodeCodec.start();
比特率:可以理解成264文件的码率,即时间单位中编码所占的字节数,越高代表画质越好,记录下来的细节越多,但同样的文件也就越大,他的单位是kbps,算法是 文件大小M/时间秒 得出来
帧率:用FPS表示,用于表示一秒有多少帧,与画面得流畅度相关,如果帧数越多,画面衔接越好,同样帧数越多文件越大
2.获取YUV数据,放到编码器中进行编码
Camera1获取得数据格式是NV21,需要转换成MediaCodec可以操作得NV12格式才可以传给编码器进行操作
//获取当前的yuv数据
if (!demo6Activity.getYUVQueue().isEmpty()) {
//从猪肉工厂获取装猪的小推车,填充数据后发送到猪肉工厂进行处理
ByteBuffer[] inputBuffers = encodeCodec.getInputBuffers();//所有的小推车
int inputIndex = encodeCodec.dequeueInputBuffer(0);//返回当前可用的小推车标号
if (inputIndex != -1) {
Log.i(LOG_TAG, "找到了input 小推车" + inputIndex);
ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到小推车
inputBuffer.clear();//扔出去里面旧的东西
//从yuv队列中取出数据
byte[] yuvData = demo6Activity.getYUVQueue().poll();
//摄像头默认是横着的,需要旋转90度
byte[] revolveData = revolveYuv(yuvData);
byte[] yuv420sp = new byte[width * height * 3 / 2];
//把待编码的视频帧转换为YUV420格式
NV21ToNV12(revolveData, yuv420sp, height, width);
//audioExtractor没猪了,也要告知一下
if (revolveData.length < 0) {
Log.i(LOG_TAG, "当前yuv数据异常");
} else {//拿到猪
//把转换后的YUV420格式的视频帧放到编码器输入缓冲区中
Log.i(LOG_TAG, "yuv数据转换成功,当前数据的数据长度为:" + yuvData.length);
inputBuffer.limit(revolveData.length);
inputBuffer.put(revolveData, 0, yuvData.length);
//计算时间戳,配置了但是没有用,所以此处传0也可以
pts = computePresentationTime(generateIndex);
Log.i(LOG_TAG, "当前时间戳:" + pts);
encodeCodec.queueInputBuffer(inputIndex, 0, revolveData.length, pts, 0);
generateIndex += 1;
}
} else {
Log.i(LOG_TAG, "没有可用的input 小推车");
}
}
//在使用Camera的时候,设置预览的数据格式为NV21,转换成nv12
private byte[] NV21ToNV12(byte[] nv21, byte[] nv12, int width, int height) {
if (nv21 == null || nv12 == null) return null;
int framesize = width * height;
int i = 0, j = 0;
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];
}
return nv12;
}
3.将编码好得数据写入到h264文件中
SPS和PPS等配置信息是放在I帧前面得,如果读取到这些配置信息,需要先将这些信息存储下来,等到I帧编码出来,为每个I帧前面添加上这些配置信息,这样在另一端接受时就可以从任何一个I帧前面找到这些配置信息来配置播放器
int outputIndex = encodeCodec.dequeueOutputBuffer(encodeBufferInfo, 10000);//返回当前筐的标记
switch (outputIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.i(LOG_TAG, "输出的format已更改" + encodeCodec.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.i(LOG_TAG, "超时,没获取到");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.i(LOG_TAG, "输出缓冲区已更改");
break;
default:
Log.i(LOG_TAG, "获取到编码后的数据了,当前解析后的数据长度为:" + encodeBufferInfo.size);
//获取所有的筐
ByteBuffer[] outputBuffers = encodeCodec.getOutputBuffers();
//拿到当前装满火腿肠的筐
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
outputBuffer = encodeCodec.getOutputBuffer(outputIndex);
} else {
outputBuffer = outputBuffers[outputIndex];
}
//将数据读取到outData中
byte[] outData = new byte[encodeBufferInfo.size];
outputBuffer.get(outData);
//当前是初始化编解码器数据,不是媒体数据,sps、pps等初始化数据
if (encodeBufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG) {
Log.i(LOG_TAG, "配置信息编码完成");
configByte = new byte[encodeBufferInfo.size];
configByte = outData;
} else if (encodeBufferInfo.flags == BUFFER_FLAG_KEY_FRAME) {//当前的数据中包含关键帧数据
Log.i(LOG_TAG, "I帧编码完成");
//将初始化数据和当前的关键帧数据合并后写入到h264文件中
byte[] keyframe = new byte[encodeBufferInfo.size + configByte.length];
System.arraycopy(configByte, 0, keyframe, 0, configByte.length);
//把编码后的视频帧从编码器输出缓冲区中拷贝出来
System.arraycopy(outData, 0, keyframe, configByte.length, outData.length);
fos.write(keyframe, 0, keyframe.length);
} else {
//写到文件中
Log.i(LOG_TAG, "非I帧数据编码完成");
fos.write(outData, 0, outData.length);
}
//把筐放回工厂里面
encodeCodec.releaseOutputBuffer(outputIndex, false);
break;
}
if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(LOG_TAG, "表示当前编解码已经完事了");
inOutFinish = true;
}
最后把SD卡中保存得264文件通过专门的工具播放出来,此处使用的时雷霄骅大神的VideoEye
上面我们通过IO自己将编码好的264码流写入文件中,我们还可以通过MediaMuter来帮我们写入
out264File = new File(demo6Activity.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "cameraMuxer.h264");
out264File.createNewFile();
vedioMuxer = new MediaMuxer(out264File.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
......
int outputIndex = encodeCodec.dequeueOutputBuffer(encodeBufferInfo, 10000);//返回当前筐的标记
switch (outputIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.i(LOG_TAG, "输出的format已更改" + encodeCodec.getOutputFormat());
videoTrackIndex = vedioMuxer.addTrack(encodeCodec.getOutputFormat());
vedioMuxer.start();//开始合成audio
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.i(LOG_TAG, "超时,没获取到");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.i(LOG_TAG, "输出缓冲区已更改");
break;
default:
Log.i(LOG_TAG, "获取到编码后的数据了,当前解析后的数据长度为:" + encodeBufferInfo.size);
//获取所有的筐
ByteBuffer[] outputBuffers = encodeCodec.getOutputBuffers();
//拿到当前装满火腿肠的筐
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
outputBuffer = encodeCodec.getOutputBuffer(outputIndex);
} else {
outputBuffer = outputBuffers[outputIndex];
}
//将数据读取到outData中
byte[] outData = new byte[encodeBufferInfo.size];
outputBuffer.get(outData);
//当前是初始化编解码器数据,不是媒体数据,sps、pps等初始化数据
if (encodeBufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG) {
configByte = new byte[encodeBufferInfo.size];
configByte = outData;
} else if (encodeBufferInfo.flags == BUFFER_FLAG_KEY_FRAME) {//当前的数据中包含关键帧数据
//将初始化数据和当前的关键帧数据合并后写入到h264文件中
byte[] keyframe = new byte[encodeBufferInfo.size + configByte.length];
System.arraycopy(configByte, 0, keyframe, 0, configByte.length);
//把编码后的视频帧从编码器输出缓冲区中拷贝出来
System.arraycopy(outData, 0, keyframe, configByte.length, outData.length);
ByteBuffer newBuffer = ByteBuffer.allocate(keyframe.length);
newBuffer.put(keyframe);
videoBufferInfo.size = keyframe.length;
videoBufferInfo.presentationTimeUs = (System.nanoTime() - startTime) / 1000;
videoBufferInfo.offset = 0;
videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
//通过MediaMuxer写入
vedioMuxer.writeSampleData(videoTrackIndex, newBuffer, videoBufferInfo);
} else {
//写到文件中
videoBufferInfo.size = encodeBufferInfo.size;
videoBufferInfo.presentationTimeUs = (System.nanoTime() - startTime) / 1000;
videoBufferInfo.offset = 0;
videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
//通过MediaMuxer写入
vedioMuxer.writeSampleData(videoTrackIndex, outputBuffer, videoBufferInfo);
}
//把筐放回工厂里面
encodeCodec.releaseOutputBuffer(outputIndex, false);
break;
}
if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(LOG_TAG, "表示当前编解码已经完事了");
inOutFinish = true;
}
通过MediaMuter输出的264文件各项数据都时非常正常的,而通过io流写出的数据则从文件大小,时间戳,比特率方面都有一些问题,暂时还没有去研究具体的原因,可以放到后面再深入研究一些