因为这里会用到,所以先介绍这个的用法。
MediaCodec类用于使用一些基本的多媒体编解码操作。
主要的API如下:
在编码压缩的过程中,有几个关键的标志如下:
AVC是一种编码。实际上是 H.264 协议的别名。自从H.264协议中增加了SVC的部分之后,人们习惯将不包含SVC的H.264协议那一部分称为 AVC,而将SVC这一部分单独称为SVC。
ArrayBlockingQueue是一个阻塞式的队列,线程安全,底层以数组的形式保存数据,其所含的对象是以FIFO(先入先出)顺序排序的。
YUV420是一类数据格式的总称。不仅仅只是下面给出的 :
YUV420有平面格式(Planar),即Y、U、V是分开存储的,其中Y为 width*height,而U、V合占Y的一半,该种格式每个像素占12比特。根据U、V的顺序,分出2种格式,U前V后即YUV420P,也叫 I420。V前U后,叫YV12(YV表示Y后面跟着V,12表示12bit)。
还有一种半平面格式(Semi-planar),即Y单独占一块地 方,其后U、V紧挨着排在一起,根据U、V的顺序,又有2种,( U前V后叫NV12,在国内好像很多人叫它为YUV420SP格式);V前U后叫 NV21。
I420: YYYYYYYY UU VV =>YUV420P
YV12: YYYYYYYY VV UU =>
NV12: YYYYYYYY UVUV =>YUV420SP
NV21: YYYYYYYY VUVU =>
所以由此得出:YUV420 数据在内存中的长度是 width * hight * 3 / 2 (Y占1、UV占0.5)
我们之后会用到 NV21格式向 NV12格式转换,原因在Android-音视频(5):用 Camera API 采集视频数据并用SurfaceView显示 中有说。
MainActivity.java
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback, Camera.PreviewCallback {
//预览用SurfaceView,视频采集用Camera类,视频压缩为H.264(avc)
Camera camera;
SurfaceView surfaceView;
SurfaceHolder surfaceHolder;
Button stopEncoder;
int width = 1280;
int height = 720;
int framerate = 30; //一秒30帧
H264Encoder encoder; //自定义的编码操作类
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main5);
stopEncoder = (Button) findViewById(R.id.stopEncoder);
surfaceView = (SurfaceView) findViewById(R.id.surface_view);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(this);
if (supportH264Codec()){ //查询手机是否支持AVC编码
Log.e("TAG" , "support H264 hard codec");
}else {
Log.e("TAG" , "not support H264 hard codec");
}
stopEncoder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.i("TAG", "surfaceDestroyed Medthod");
//停止预览,并释放资源
if (camera != null){
camera.setPreviewCallback(null);
camera.stopPreview();
camera = null;
}
if (encoder != null){
//停止编码
encoder.stopEncoder();
}
}
});
}
private boolean supportH264Codec() {
// 遍历支持的编码格式信息,并查询有没有支持H.264(avc)的编码
if (Build.VERSION.SDK_INT >= 18){
//计算可用的编解码器数量
int number = MediaCodecList.getCodecCount();
for (int i=number-1 ; i >0 ; i--){
//获得指定的编解码器信息
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
//得到支持的类型
String[] types = codecInfo.getSupportedTypes();
//查询有没有支持H.264(avc)的编码
for (int j = 0 ; j < types.length ; j++){
if (types[j].equalsIgnoreCase("video/avc")){
return true;
}
}
}
}
return false;
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
Log.i("TAG", "surfaceCreated Medthod");
camera = Camera.open(); //开启相机
camera.setDisplayOrientation(90);
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21); //设置数据格式
parameters.setPreviewSize(1280,720);
try{
camera.setParameters(parameters);
camera.setPreviewDisplay(surfaceHolder);
camera.setPreviewCallback(this);
camera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
//编码初始化
encoder = new H264Encoder(width,height,framerate);
encoder.startEncoder(); //开始编码
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
Log.i("TAG", "surfaceChanged Medthod");
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
/*Log.i("TAG", "surfaceDestroyed Medthod");
//停止预览,并释放资源
if (camera != null){
camera.setPreviewCallback(null);
camera.stopPreview();
camera = null;
}
if (encoder != null){
//停止编码
encoder.stopEncoder();
}*/
}
@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
//返回相机预览的视频数据,并给H264Encoder编码压缩为H.264(avc)的文件test.mp4
//这里面的Bytes的数据就是NV21格式的数据
if (encoder != null){
encoder.putDate(bytes); //将一帧的数据传过去处理
}
}
}
H264Encoder.java
public class H264Encoder {
private final static int TIMEOUT_USEC = 12000; //超时时间
private MediaCodec mediaCodec; //核心
public boolean isRunning = false; //flag
private int width ,height , framerate;
public byte[] configbyte;
private BufferedOutputStream outputStream;
//存储camera返回的视频数据yuv(NV21)
public ArrayBlockingQueue yuv420queue = new ArrayBlockingQueue(10);
public H264Encoder(int width, int height, int framerate) {
this.width = width;
this.height = height;
this.framerate = framerate;
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc",width,height);
//设置编码器的数据格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar); //NV12(YUV420SP)的数据格式
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE,width*height*5); //比特率 bite/s
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1);
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
//创建编码器
mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
createfile();//准备存储视频录制数据的文件
} catch (IOException e) {
e.printStackTrace();
}
}
private void createfile() {
String path = Environment.getExternalStorageDirectory().getAbsolutePath()+ File.separator+"test.mp4";
File file = new File(path);
if (file.exists()){
file.delete();
}
try {
outputStream = new BufferedOutputStream(new FileOutputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
//核心
public void startEncoder() {
new Thread(new Runnable() {
@Override
public void run() {
isRunning = true;
byte[] input = null;
long pts = 0;
long generateIndex = 0 ;
while (isRunning){
if (yuv420queue.size() > 0){
//得到一帧数据
input = yuv420queue.poll();
//YUV420 数据在内存中的长度是 width * hight * 3 / 2 (Y占UV占0.5)
byte[] yuv420sp = new byte[width*height*3/2];
//必须要转换格式,否则视频录得内容播放出来颜色有偏差
NV21TONV12(input,yuv420sp,width,height);
input = yuv420sp;
}
if (input != null){
try {
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0){
//当前帧的时间戳
pts = computePresentationTime(generateIndex);
//得到编码的输入缓冲区
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
//向缓冲区添加数据
inputBuffer.put(input);
//缓冲区数据入编码器
mediaCodec.queueInputBuffer(inputBufferIndex,0,input.length,pts,0);
generateIndex += 1;
}
//定义一个BufferInfo保存outputBufferIndex的帧信息
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
while (outputBufferIndex >= 0){
//得到输出缓冲区
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size];
//将数据写入outData
outputBuffer.get(outData);
if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG){
//关键帧信息或初始化的信息
configbyte = new byte[bufferInfo.size];
configbyte = outData;
} else if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_SYNC_FRAME){
//关键帧
byte[] keyframe = new byte[bufferInfo.size + configbyte.length];
System.arraycopy(configbyte,0,keyframe,0,configbyte.length);
System.arraycopy(outData,0,keyframe,configbyte.length,outData.length);
outputStream.write(keyframe,0,keyframe.length);
}else {
outputStream.write(outData,0,outData.length);
}
//释放输出缓冲区,进行下一次编码操作
mediaCodec.releaseOutputBuffer(outputBufferIndex,false);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
}
} catch (IOException e) {
e.printStackTrace();
}
}else {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//编解码完成,停止编解码,并释放资源
mediaCodec.stop();;
mediaCodec.release();
//关闭数据流
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
private long computePresentationTime(long frameIndex) {
return frameIndex * 1000000 / framerate;
}
private void NV21TONV12(byte[] nv21, byte[] nv12, int width, int height) {
if (nv21 == null || nv12 == null) return;
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 = 10){
yuv420queue.poll();
}
yuv420queue.add(bytes);
}
}
简单来说由于输出的是纯H264(AVC)的文件,是没有时间戳概念的,就是一堆流文件,所以如果手机配置低的话,由于编码时间过长(大于两次帧刷新间隔的话),会导致很多帧数据丢掉,所以最终的流文件播放出来就会短。
前置摄像头: camera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
解决办法:
不是camera.setDisplayOrientation(90)的问题,这个不管是前置还是后置,预览正常都需要加上这句话。
手机相机录出来的数据本身就是横着的,要作的处理是,将前置相机回调的图像数据(NV21)旋转270°之后再写入到编码器进行编码,这样输出的H264流就是角度正常的。
旋转方法如下:
private byte[] rotateYUV420Degree270(byte[] data, int imageWidth, int imageHeight){
byte[] yuv =new byte[imageWidth*imageHeight*3/2];
// Rotate the Y luma
int i =0;
for(int x = imageWidth-1;x >=0;x--){
for(int y =0;y < imageHeight;y++){
yuv[i]= data[y*imageWidth+x]; i++;
} }// Rotate the U and V color components i = imageWidth*imageHeight;
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-1)]; i++; yuv[i]= data[(imageWidth*imageHeight)+(y*imageWidth)+x]; i++;
}
}
return yuv;
}
使用位置如下:自己在上面的代码找对应的位置
@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
//返回相机预览的视频数据,并给H264Encoder编码,压缩成H.264(avc)格式的文件test.mp4
//这里面的Bytes的数据就是NV21格式的数据(YUV)
if (encoder != null) {
encoder.putDate(rotateYUV420Degree270(bytes,1280,720));
}
}
由于旋转了270度后,视频数据的宽高正好对换,所以要在H264Encoder类中中修改初始化MediaCodec时的宽高。
public H264Encoder(int width, int height, int framerate) {
this.width = width;
this.height = height;
this.framerate = framerate;
//在这一行注意将宽高对换,不然视频播放错误
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc",height,width);
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,30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,1);
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
createfile();
} catch (IOException e) {
e.printStackTrace();
}
}
注意:
不管前置还是后置摄像头,录制的视频文件播放时图像都是倒着的,所以都需要上面的解决棒法解决。