天天看點

Android平台采集攝像頭圖像和使用MediaCodec寫死的例子詳解

很多Android系統上的應用需要采集攝像頭圖像,并把圖像編碼成某種格式(比如H264),儲存成檔案或發送到網絡。雖然有FFmpeg可以實作編碼的功能,但是使用軟編碼一方面比較耗電,另一方面,對于CPU性能不是太強的ARM裝置來說,軟體編碼肯定很占CPU資源,有些舊的機器甚至一編碼就卡機。從Android 4.1系統起,引進了MediaCodec API,可以實作寫死的功能,這解決了廣大程式員編碼視訊的難題,但是這套API使用起來并不簡單,有很多細節要注意,很容易掉進一些坑,下面我會講解怎麼越過那些坑,但是首先我先用代碼說明一下怎麼在Android上調用攝像頭的API來采集圖像。

  1. 采集攝像頭圖像
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來進行音視訊的編解碼工作了。

Android平台采集攝像頭圖像和使用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

繼續閱讀