本篇文章记录一下,Android调用mediacodec编码camera回掉的YUV数据为h264的方法。
由于公司需要,软编码(X264)由于手机性能的瓶颈,已不能满足要求,所以决定使用硬编码。其实硬编码最早用过MediaRecord,但是不能直接得到h264数据,得先编成MP4,再从MP4里把H264的NALU取出来,感觉太绕了,所以当时抛弃了MediaRecord,选择了x264。不过看来,现在还得走上硬编码的路了 -- MediaCodec
这篇文章就用一个demo来说一下mediacodec的调用吧。
首先,要获取到CAMERA的回掉回来的YUV数据。
其次,将获得到的数据用MEDIACODEC编码为H264。
最后,将H264写入文件,程序结束后,可用VLC等支持播放H264的播放器查看效果。
先说下获取YUV数据吧,这个很简单了,直接上代码
packagecom.example.mediacodecencode;
importjava.io.IOException;
importjava.util.ArrayList;
importjava.util.concurrent.ArrayBlockingQueue;
importandroid.annotation.SuppressLint;
importandroid.annotation.TargetApi;
importandroid.app.Activity;
importandroid.graphics.ImageFormat;
importandroid.hardware.Camera;
importandroid.hardware.Camera.Parameters;
importandroid.hardware.Camera.PreviewCallback;
importandroid.media.MediaCodecInfo;
importandroid.media.MediaCodecList;
importandroid.os.Build;
importandroid.os.Bundle;
importandroid.util.Log;
importandroid.view.SurfaceHolder;
importandroid.view.SurfaceView;
publicclassMainActivityextendsActivityimplementsSurfaceHolder.Callback,PreviewCallback{
privateSurfaceView surfaceview;
privateSurfaceHolder surfaceHolder;
privateCamera camera;
privateParameters parameters;
intwidth =1280;
intheight =720;
intframerate =30;
intbiterate =8500*1000;
privatestaticintyuvqueuesize =10;
publicstaticArrayBlockingQueue
YUVQueue =newArrayBlockingQueue
(yuvqueuesize);
privateAvcEncoder avcCodec;
@Override
protectedvoidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
surfaceview = (SurfaceView)findViewById(R.id.surfaceview);
surfaceHolder = surfaceview.getHolder();
surfaceHolder.addCallback(this);
SupportAvcCodec();
}
@Override
publicvoidsurfaceCreated(SurfaceHolder holder) {
camera = getBackCamera();
startcamera(camera);
avcCodec =newAvcEncoder(width,height,framerate,biterate);
avcCodec.StartEncoderThread();
}
@Override
publicvoidsurfaceChanged(SurfaceHolder holder,intformat,intwidth,intheight) {
}
@Override
publicvoidsurfaceDestroyed(SurfaceHolder holder) {
if(null!= camera) {
camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
camera =null;
avcCodec.StopThread();
}
}
@Override
publicvoidonPreviewFrame(byte[] data, android.hardware.Camera camera) {
// TODO Auto-generated method stub
putYUVData(data,data.length);
}
publicvoidputYUVData(byte[] buffer,intlength) {
if(YUVQueue.size() >=10) {
YUVQueue.poll();
}
YUVQueue.add(buffer);
}
@SuppressLint("NewApi")
privatebooleanSupportAvcCodec(){
if(Build.VERSION.SDK_INT>=18){
for(intj = MediaCodecList.getCodecCount() -1; j >=0; j--){
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(j);
String[] types = codecInfo.getSupportedTypes();
for(inti =0; i < types.length; i++) {
if(types[i].equalsIgnoreCase("video/avc")) {
returntrue;
}
}
}
}
returnfalse;
}
privatevoidstartcamera(Camera mCamera){
if(mCamera !=null){
try{
mCamera.setPreviewCallback(this);
mCamera.setDisplayOrientation(90);
if(parameters ==null){
parameters = mCamera.getParameters();
}
parameters = mCamera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21);
parameters.setPreviewSize(width, height);
mCamera.setParameters(parameters);
mCamera.setPreviewDisplay(surfaceHolder);
mCamera.startPreview();
}catch(IOException e) {
e.printStackTrace();
}
}
}
@TargetApi(9)
privateCamera getBackCamera() {
Camera c =null;
try{
c = Camera.open(0);// attempt to get a Camera instance
}catch(Exception e) {
e.printStackTrace();
}
returnc;// returns null if camera is unavailable
}
}
其实没啥说的,很简答的逻辑。不过上面代码有这么几点可以说一下:
1.camera start的时机最好放在surfaceCreated,销毁最好放在surfaceDestroyed;
2.camera parameters setPreviewFormat的时候在5.0一下系统使用NV21或YV12,因为基本所有的安卓手机都支持这两种预览格式;
3.最好在程序的开始,判断一下系统是否支持MediaCodec编码h264,具体逻辑可见上面的SupportAvcCodec方法。
4.上面的代码中,可以看出,我把YUV数据放到一个队列里面了,准备使用。
其次就是使用MediaCodec编码h264了,首先,初始化MediaCodec,方法如下:
@SuppressLint("NewApi")
publicAvcEncoder(intwidth,intheight,intframerate,intbitrate) {
m_width = width;
m_height = height;
m_framerate = framerate;
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
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");
}catch(IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
mediaCodec.configure(mediaFormat,null,null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
createfile();
}
需要注意的一点是,对于比特率,其实完全可以这样处理,N*width*height,N可设置为1 2 3或者1 3 5等,来区分低/中/高的码率。
另外,我选择了YUV420SP作为编码的目标颜色空间,其实YUV420SP就是NV12,咱们CAMERA设置的是NV21,所以需要转一下。转换方法如下:
privatevoidNV21ToNV12(byte[] nv21,byte[] nv12,intwidth,intheight){
if(nv21 ==null|| nv12 ==null)return;
intframesize = width*height;
inti =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];
}
}
下面,就是编码的函数了,我这里把编码放在一个线程里,去轮训YUV队列,如有有数据就编码,具体如下:
publicvoidStartEncoderThread(){
Thread EncoderThread =newThread(newRunnable() {
@SuppressLint("NewApi")
@Override
publicvoidrun() {
isRuning =true;
byte[] input =null;
longpts =0;
longgenerateIndex =0;
while(isRuning) {
if(MainActivity.YUVQueue.size() >0){
input = MainActivity.YUVQueue.poll();
byte[] yuv420sp =newbyte[m_width*m_height*3/2];
NV21ToNV12(input,yuv420sp,m_width,m_height);
input = yuv420sp;
}
if(input !=null) {
try{
longstartMs = System.currentTimeMillis();
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
intinputBufferIndex = 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;
}
MediaCodec.BufferInfo bufferInfo =newMediaCodec.BufferInfo();
intoutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
while(outputBufferIndex >=0) {
//Log.i("AvcEncoder", "Get H264 Buffer Success! flag = "+bufferInfo.flags+",pts = "+bufferInfo.presentationTimeUs+"");
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData =newbyte[bufferInfo.size];
outputBuffer.get(outData);
if(bufferInfo.flags ==2){
configbyte =newbyte[bufferInfo.size];
configbyte = outData;
}elseif(bufferInfo.flags ==1){
byte[] keyframe =newbyte[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(Throwable t) {
t.printStackTrace();
}
}else{
try{
Thread.sleep(500);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
EncoderThread.start();
}
需要注意的有两点,其实也是两个坑:
坑1:mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0); 第四个参数,是否需要传入?我觉得必须得传,因为不传的话,你就会发现mediaCodec.dequeueOutputBuffer变了第一个I帧之后,一直返回-1。
坑2:关于mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC)的超时时间是否要传,穿多少?我觉得不能传-1(不能丢帧,一直等),传-1会卡住,要么编码非常卡,传多少合适呢,传11000吧,下过不错。
下面贴一下计算PTS的方法:
/**
* Generates the presentation time for frame N, in microseconds.
*/
private long computePresentationTime(long frameIndex) {
return 132 + frameIndex * 1000000 / m_framerate;
}
这样,大概就说完了,其实也很简单,不过,就是编码的时候一些参数的设置非常重要,例如一款硬件比较差的设备,那么帧率就得设置的低一些,码率也一样。
如果发现编码出来之后,播放很卡,那么请降低帧率,降低码率。
在github上面穿了例子,地址如下:
https://github.com/sszhangpengfei/MediaCodecEncodeH264
原文地址:http://blog.csdn.net/ss182172633/article/details/50256733