很多Android系統上的應用需要采集攝像頭圖像,并把圖像編碼成某種格式(比如H264),儲存成檔案或發送到網絡。雖然有FFmpeg可以實作編碼的功能,但是使用軟編碼一方面比較耗電,另一方面,對于CPU性能不是太強的ARM裝置來說,軟體編碼肯定很占CPU資源,有些舊的機器甚至一編碼就卡機。從Android 4.1系統起,引進了MediaCodec API,可以實作寫死的功能,這解決了廣大程式員編碼視訊的難題,但是這套API使用起來并不簡單,有很多細節要注意,很容易掉進一些坑,下面我會講解怎麼越過那些坑,但是首先我先用代碼說明一下怎麼在Android上調用攝像頭的API來采集圖像。
- 采集攝像頭圖像
public class MainActivity extends Activity {
private SurfaceView surfaceView;
private Camera camera;
private SurfaceHolder surfaceHolder;
private Camera.Parameters mParameters = null;
@SuppressLint("InlinedApi")
private int mCameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
// 不同手機有不同的比對顔色
private int width = 1280;
private int height = 720;
private int mFrameRate = 25;
private int bitrate = 800*1024;
byte[] h264 = new byte[width * height * 3 / 2];
byte[] mPreBuffer = null;
private AvcEncode avcEncode;
private Button button1;
private Button button2;
private TextView labelFrameRate;
private TextView labelCPULevel;
Timer timer = null;
Boolean m_bRunning = false;
private long mLastStartCountTime = 0;
private int mFrameCapturedCountInCycle = 0;
static {
// System.loadLibrary("gnustl_shared");
System.loadLibrary("songstudio");
}
//EncodeVideo EncoderJni = new EncodeVideo(); //建構軟解編碼器
@SuppressWarnings("deprecation")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.activity_main);
button1 = (Button) findViewById(R.id.btn_start_preview);
button2 = (Button) findViewById(R.id.btn_stop_preview);
labelFrameRate = (TextView) findViewById(R.id.label_framerate);
labelCPULevel = (TextView) findViewById(R.id.label_cpu_level);
avcEncode = new AvcEncode(width, height, mFrameRate, bitrate, 20);
//EncoderJni.SetFilePath(FileOperator.fileName2);
//EncoderJni.SetParameters(width, height, mFrameRate, bitrate, 25);
surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.setFixedSize(width, height);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
surfaceHolder.addCallback(new SurfaceCallback());
// Sampler.getInstance().init(getApplicationContext(), 3000L);
// Sampler.getInstance().start();
button1.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// TODO 自動生成的方法存根
//EncoderJni.OpenEncoder();
avcEncode.startEncode();
camera.startPreview();
mLastStartCountTime = System.currentTimeMillis();
mFrameCapturedCountInCycle = 0;
if(timer != null){
timer.cancel();
timer = null;
}
timer = new Timer();
timer.schedule(new TimerTask() {
//int i=10;
@Override
public void run() {
if(m_bRunning)
{
Message message = new Message();
message.what = 1;
handler.sendMessage(message);
}
else
{
timer.cancel();
System.out.println("timer canceled!");
timer = null;
}
} //run
},
500, 2000);
m_bRunning = true;
}
});
button2.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if(camera != null && m_bRunning){
camera.stopPreview();
}
avcEncode.close();
//EncoderJni.CloseEncoder();
m_bRunning = false;
}
});
}
protected void OnDestroy() {
if(camera != null){
camera.stopPreview();
}
//EncoderJni.CloseEncoder();
avcEncode.close();
m_bRunning = false;
super.onDestroy();
}
Handler handler = new Handler() {
public void handleMessage(Message msg) {
long CurrentTime = System.currentTimeMillis();
if(CurrentTime - mLastStartCountTime > 2000)
{
long nAverateFrameRate = mFrameCapturedCountInCycle*1000/(CurrentTime - mLastStartCountTime);
labelFrameRate.setText(Integer.toString((int)nAverateFrameRate));
mLastStartCountTime = CurrentTime;
mFrameCapturedCountInCycle = 0;
}
labelCPULevel.setText(Integer.toString((int)Sampler.getInstance().m_lastCPULevel));
super.handleMessage(msg);
};
};
private final class SurfaceCallback implements Callback, PreviewCallback {
@SuppressLint("NewApi")
public void surfaceCreated(SurfaceHolder holder) {
// TODO 自動生成的方法存根
try {
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
camera = Camera.open();
} else {
camera = Camera.open(mCameraId);
}
camera.setPreviewDisplay(surfaceHolder); //是否預覽顯示
//camera.setDisplayOrientation(90);
mParameters = camera.getParameters();
mParameters.setPreviewSize(width, height);
mParameters.setPictureSize(width, height);
// 此處顔色設定與後面要比對 否則顔色會出現變化
mParameters.setPreviewFormat(ImageFormat.YV12);
camera.setParameters(mParameters);
int size = width * height * 3 / 2;
if (mPreBuffer == null) {
mPreBuffer = new byte[size];
}
camera.addCallbackBuffer(mPreBuffer);
camera.setPreviewCallbackWithBuffer(this);
// camera.startPreview();
} catch (Exception e) {
Log.e("TEST", "setPreviewDisplay fail " + e.getMessage());
}
}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// TODO 自動生成的方法存根
}
public void surfaceDestroyed(SurfaceHolder holder) {
// TODO 自動生成的方法存根
camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
camera = null;
avcEncode.close();
}
public void onPreviewFrame(byte[] data, Camera camera) {
avcEncode.offerEncode(data, h264);
//avcEncode.CompressBuffer(data, h264);
mFrameCapturedCountInCycle++;
//EncoderJni.PushData(data, 0);
camera.addCallbackBuffer(mPreBuffer);
}
}
}
Camera是一個專門用來連接配接和斷開相機服務的類,用來處理相機相關的事件。上面的SurfaceView用于預覽圖像,并且重載了Camera.PreviewCallback接口的onPreviewFrame函數,用于獲得捕捉到的圖像,而圖像的格式是根據mParameters.setPreviewFormat來指定,這是是設定為YV12;而預覽的圖像大小和采集的圖像大小分别通過mParameters.setPreviewSize和mParameters.setPictureSize來設定。另外,我們還調用了addCallbackBuffer來重用Buffer,如果不調用這個API,則回調函數onPreviewFrame調用的時候每次都會配置設定一塊記憶體傳遞圖像資料,這樣頻繁配置設定記憶體可能會造成比較多的記憶體碎片,降低系統性能,如果調用了addCallbackBuffer對Buffer進行循環利用,系統核心調用回調時就不會重新配置設定記憶體,是以建議采集圖像時還是盡量使用addCallbackBuffer方法(雖然有點麻煩)。
除了SurfaceView,我們還可以使用SurfaceTexture來獲得圖像流,雖然我們這個代碼裡沒有用到。SurfaceTexture是從Android3.0(API 11)加入的一個新類,跟SurfaceView很像,可以從camera preview或者video decode裡面擷取圖像流(image stream)。SurfaceView從camera讀取到的預覽(preview)圖像流一定要輸出到一個可見的(Visible)SurfaceView上,而SurfaceTexture在接收圖像流後,不需要顯示出來。是以在有些需求上,比如在Service背景裡面擷取camera預覽幀進行處理就必須使用SurfaceTexture才能實作。關于SurfaceTexture的使用,大家可參考這篇博文:https://blog.csdn.net/u012874222/article/details/70216700。
2.使用MediaCodec編碼
MediaCodec的使用在Android Developer官網上有詳細的說明。官網上的圖能夠很好的說明MediaCodec的使用方式。我們隻需了解這個圖,然後熟悉下MediaCodec的API就可以很快的上手使用MediaCodec來進行音視訊的編解碼工作了。
我們還需要知道,MediaCodec總共有三種使用方法:
同步資料處理(使用buffer arrays) 從Android4.1 api 16即可以使用;
同步資料處理 (使用buffers ) 從Android5.0 api 21 即可以使用;
異步資料處理(使用buffers ) 從Android5.0 api 21 即可以使用;
同步與異步處理資料的主要不同點是:對于同步處理是循環的将待處理資料交給編解碼器(不用理會編解碼器是否已經準備好接收資料),而異步處理則是每次都會去等待編解碼器已經能夠處理資料時,才将待處理資料送給相應編解碼器。
第一種和第二種方法差別不大,而第一種方法适用性更廣,因為對API的Level要求低。而第三種方法對性能要求比較高的場合可考慮使用,因為它的編碼是異步的,編碼操作是内部進行,是以不會卡住原先的線程,而如果把采集圖像和編碼都放在一個線程(就像方法一和方法二),則可能出現一個線程做的工作太多,導緻效率低了。
為了簡單起見,我的例子使用了方法一。
使用MediaCodec前要初始化編碼器的格式,下面是初始化的代碼:
public AvcEncode(int width, int height, int framerate, int bitrate, int i_frame_interval) {
this.width = width;
this.height = height;
yuv420 = new byte[width * height * 3 / 2];
m_FrameRate = framerate;
m_Bitrate = bitrate;
bConfigBitrate = false;
try{
mediaCodec = MediaCodec.createEncoderByType("video/avc");
MediaFormat mediaFormat = null;
if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_720P)) {
mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
} else {
mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
}
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
// 根據手機設定不同的顔色參數
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
// mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaCodec.configure(mediaFormat, null, null,MediaCodec.CONFIGURE_FLAG_ENCODE);
m_outData = new byte[width*height*2];
}catch(Exception e){
e.printStackTrace();
}
}
這裡有幾個很重要的參數,包括:碼率模式,碼率,幀率,I幀間隔,碼率的機關是比特的,例如:512000,表示碼率是512KBps。I幀間隔是設定相鄰兩個I幀的間隔,這裡的機關是秒,比如1秒,表示每隔1秒插入一個I幀。而Profile和Level這裡沒有設定,因為在Android7.0以下,設定這兩個參數在很多裝置上是無效的,如果設定了,可能會導緻後面的configure函數失敗。這裡有必要說明一下,很多讀者遇到一個坑,就是設定的碼率不生效,其實這個是跟設定的幀率有關系,如果設定的幀率為25(就算你手機的攝像頭實際采集的幀率達不到這麼高也沒關系),則實際碼率就跟設定的一樣了。
從攝像頭采集到的圖像資料在回調函數OnPreviewFrame中獲得,裡面調用了編碼函數對圖像進行編碼,回調函數如下:
public void onPreviewFrame(byte[] data, Camera camera) {
avcEncode.offerEncode(data, h264);
//avcEncode.CompressBuffer(data, h264);
mFrameCapturedCountInCycle++;
//EncoderJni.PushData(data, 0);
camera.addCallbackBuffer(mPreBuffer);
}
使用MediaCodec編碼的函數如下:
public void offerEncode(byte[] input, byte[] output) {
int pos = 0;
swapYV12toI420(input, yuv420, width, height);
try {
int inputBufferIndex = mediaCodec.dequeueInputBuffer(0);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(yuv420);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, yuv420.length,
(System.currentTimeMillis() - start_timeStamp)*1000, 0);
}
else{
return;
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
int n = m_outData.length;
outputBuffer.get(m_outData, 0, bufferInfo.size);
int nalu_type = (m_outData[4] & 0x1F);
if (m_info != null) {
System.arraycopy(m_outData, 0, output, pos, bufferInfo.size);
pos += bufferInfo.size;
} else {
if (nalu_type == 0x07) {
m_info = new byte[bufferInfo.size];
System.arraycopy(m_outData, 0, m_info, 0, bufferInfo.size);
}
}
mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}//while
if (output[4] == 0x65) { //key frame, 編碼器生成關鍵幀時沒有pps sps, 要加上
if(m_info != null){
System.arraycopy(output, 0, yuv420, 0, pos);
System.arraycopy(m_info, 0, output, 0, m_info.length);
System.arraycopy(yuv420, 0, output, m_info.length, pos);
pos += m_info.length;
}
}
if(pos > 0)
addToFile(output, pos);
mFrameNum++;
} catch (Throwable t) {
t.printStackTrace();
}
}
這裡,我們調用swapYV12toI420函數将輸入的YUV圖像資料拷貝到另外一塊記憶體位址,并且将U和V兩個平面交換,這裡可能大家有點難了解,為什麼要交換呢?因為如果直接将傳入的YUV圖像顯示出來,其實色度是錯亂的,但如果将U,V交換,則顔色就正常了。
在代碼中,我們還要關注兩個細節,一個是要在開始編碼的前幾幀提取PPS和SPS,第二是每個I幀前面要插入SPS和PPS,上面的m_info數組存儲的就是SPS和PPS的内容(一般這兩個東西都是從同一幀編碼出來)。
好,寫死的用法就講完了,下面是例子工程的連結,大家可以下載下傳例子App測試一下編碼的效果。
https://download.csdn.net/download/zhoubotong2012/10555784