近期做了一段時間的Camera的開發,雖然是應用層的開發但是也粗略的接觸了一些framework相關的東西,在此寫出來分享下。
先說下應用開發的目的和實用環境,這次開發的主要目的是實現android設備上外掛USB攝像頭來實現錄像拍照的功能,類似行車記錄儀的功能,但是靈活性更大因爲可隨時在android設備上回看錄像和回看照片,並可查看GPS的運行軌跡,當然主要功能還是錄像和拍照功能了。</p><p> 首先還是要做一個簡單的demo功能出來,後面再逐漸的去豐富各個功能模塊和完善BUG。
第一步肯定是建工程了,一個主Activity用來顯示錄像預覽的圖像,並且設計簡單的按鈕分別來實現開始關閉錄像,拍照,瀏覽錄像和照片等功能。 當然首先還是要在AndroidManifest.xml中加入
<uses-permission android:name = "android.permission.CAMERA" />
<uses-feature android:name = "android.hardware.camera" />
<uses-feature android:name = "android.hardware.camera.autofocus" />
<uses-permission android:name = "android.permission.RECORD_AUDIO"></span>
之所以加入RECORD_AUDIO是因爲後期可能會加入聲音錄入,因此提前加進來。
主Activity需要實現OnClickListener,SurfaceHolder.Callback,OnCheckChangeListener等接口,因爲所設計的按鈕分別需要監聽Click,錄像模式選擇會監聽CheckBox的動作。
核心代碼
下面的函數主要是實現Camera取景預覽的界面,並且初始化了應用的初始界面,包括按鈕和預覽界面。
取景的容器爲SurfaceView,使用它必須用到SurfaceHolder,它可以隨時監聽Surface的變化。並且需要將SurfaceHolder的類型設置爲SURFACE_TYPE_PUSH_BUFFERS.這樣畫面緩存會由Camera類來管理。
private void initView() {
display = (SurfaceView) findViewById(R.id.display);
surfaceHolder = display.getHolder();
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
camera = (ImageButton) findViewById(R.id.camera);
video_record = (CheckBox) findViewById(R.id.video);
camera_shoot = (ImageButton) findViewById(R.id.shoot);
explorer = (Button) findViewById(R.id.explorer);
settings = (Button) findViewById(R.id.settings);
surfaceHolder.addCallback(this);
surfaceHolder.setFormat(PixelFormat.TRANSPARENT);
surfaceHolder.setFixedSize(1280,720);
camera.setOnClickListener(this);
camera_shoot.setOnClickListener(this);
explorer.setOnClickListener(this);
settings.setOnClickListener(this);
timer = (TextView)findViewById(R.id.timer);
video_record.setOnClickListener(this);
}
Camera錄像拍照等功能我實現均在另一個Service中,目的也是可以實現後臺錄像的功能,並隨時監聽操作。
核心代碼
此處代碼主要是完成錄像畫面的預覽,其中會進行USB設備的熱插拔監聽,拍照Camera屬性設置已經Camera對象的初始化,打開預覽等操作。
public void surfaceCreate() throws RemoteException {
Log.e(TAG, "surfaceCreate");
mSurfaceHolder = application.getSurfaceHolder();
handler.removeCallbacks(USBtask);
handler.postDelayed(USBtask, 5000);
try {
if(!screen_on){
screen_on = true;
handler.sendEmptyMessage(GPS_INTERVAL);
handler.sendEmptyMessageDelayed(RESUME, 3000);
return;
}
if(mCamera == null){
mCamera = Camera.open(cameraId);
cameraPictureParamsSet();
mCamera.setPreviewDisplay(mSurfaceHolder);
startPreview();
Log.e(TAG, "surfaceCreate mCamera is null, startPreview!");
if(isRecording){
cameraListener.autoStartRecord();
startRecord();
}
}else{
if(isRecording)
cameraListener.autoStartRecord();
mCamera.setPreviewDisplay(mSurfaceHolder);
startPreview();
Log.e(TAG, "surfaceCreate mCamera is not null, startPreview!");
}
} catch (IOException e) {
e.printStackTrace();
}catch(RuntimeException e){
cameraListener.noCamera();
}
}
這部分代碼就是錄像的主要代碼了,其中主要涉及到了MediaRecorder的部分操作,包括它輸出視頻流保存的格式和壓縮方式,碼率和碼流,分辨率等參數的設置。因此處較易出現異常因此建議多catch異常以免程序崩潰。
private void recordstart(boolean tts){
hour = 0;
minute = 0;
second = 0;
if (mMediaRecorder == null){
mMediaRecorder = new MediaRecorder();
}else{
mMediaRecorder.reset();
try {
mCamera.reconnect();
} catch (IOException e1) {
e1.printStackTrace();
}
}
cameraPictureParamsSet();
mCamera.unlock();
if(mSurfaceHolder == null)
mCamera.stopPreview();
mMediaRecorder.setCamera(mCamera);
boolean record_toggle = preference.getBoolean("record", true);
if(record_toggle){
mMediaRecorder
.setAudioSource(MediaRecorder.AudioSource.MIC);
}
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setVideoFrameRate(mVideoFrameRate);
mMediaRecorder.setVideoSize(1280, 720);
mMediaRecorder.setVideoEncodingBitRate(4000000);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
if(record_toggle){
mMediaRecorder
.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setAudioChannels(1);
mMediaRecorder.setAudioSamplingRate(8000);
}
mMediaRecorder.setMaxDuration(max_duration_ms);
mMediaRecorder.setOnErrorListener(CameraService.this);
mMediaRecorder.setOnInfoListener(CameraService.this);
getVideoName();
if(isPathValid){
try {
recordingPath = getVideoName().getAbsolutePath();
mMediaRecorder.setOutputFile(recordingPath);
fileUtils.setRecordingfile(recordingPath);
mMediaRecorder.prepare();
mMediaRecorder.start();
handler.removeCallbacks(task);
if(isPathValid){
handler.postDelayed(task, 1000);
}
cameraListener.autoStartRecord();
if(tts)
tts(getString(R.string.isrecodering));
isRecording = true;
} catch (IllegalStateException e) {
recordFail();
}catch(IOException e){
recordFail();
} catch (RemoteException e) {
e.printStackTrace();
}catch(RuntimeException e){
recordFail();
}
}else{
handler.removeCallbacks(task);
recordFail();
}
}
private void releaseRecord(){
mMediaRecorder.setOnErrorListener(null);
mMediaRecorder.setOnInfoListener(null);
mMediaRecorder.release();
mMediaRecorder = null;
handler.removeCallbacks(task);
fileUtils.setRecordingfile("");
}
public void recordStop(){
try {
isRecording = false;
mMediaRecorder.stop();
mMediaRecorder.reset();
//mCamera.lock();
mCamera.reconnect();
releaseRecord();
mCamera.setTrechometer(mTrechometer);
mTrechometer++;
mCamera.setCoordinate(1, mCoordinate1, mCoordinate2);
mCoordinate1++;
mCoordinate2 = mCoordinate2 + 1*60;
} catch (Exception e) {
e.printStackTrace();
}
}
public void takephoto(){
try {
mCamera.autoFocus(null);
} catch (RuntimeException e) {
e.printStackTrace();
}
mCamera.takePicture(new ShutterCallback() {
@Override
public void onShutter() {
}
}, null, pictureCallback);
}
以上差不多是程序裏比較核心和重要的代碼了,因爲工作原因無法公開全部代碼。但是大概流程還是很清楚的,在此重新總結下就是開發者指南中的流程
1. 打開攝像頭 —— 用Camera.open() 來獲得一個camera對象的實例。
2. 連接預覽 —— 用Camera.setPreviewDisplay()將camera連接到一個SurfaceView ,準備實時預覽。
3. 開始預覽 —— 調用 Camera.startPreview() 開始顯示實時攝像畫面。
4. 開始錄製視頻 —— 嚴格按照以下順序執行才能成功錄製視頻:
a. 解鎖Camera —— 調用Camera.unlock()解鎖,便於MediaRecorder 使用攝像頭。
b. 配置MediaRecorder —— 按照如下順序調用MediaRecorder 中的方法。詳情請參閱MediaRecorder 參考文檔。
1. setCamera() —— 用當前Camera實例將攝像頭用途設置爲視頻捕捉。
2. setAudioSource() —— 用MediaRecorder.AudioSource.CAMCORDER設置音頻源。
3. setVideoSource() —— 用MediaRecorder.VideoSource.CAMERA設置視頻源。
4. 設置視頻輸出格式和編碼格式。對於Android 2.2 (API Level 8) 以上版本,使用MediaRecorder.setProfile方法,並用CamcorderProfile.get()來獲取一個profile實例。對於Android prior to 2.2以上版本,必須設置視頻輸出格式和編碼參數:
i. setOutputFormat() —— 設置輸出格式,指定缺省設置或MediaRecorder.OutputFormat.MPEG_4。
ii. setAudioEncoder() —— 設置聲音編碼類型。指定缺省設置或MediaRecorder.AudioEncoder.AMR_NB。
iii. setVideoEncoder() —— 設置視頻編碼類型,指定缺省設置或者MediaRecorder.VideoEncoder.MPEG_4_SP。
5. setOutputFile() —— 用getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()設置輸出文件,見保存媒體文件一節中的方法示例。
6. setPreviewDisplay() —— 用上面連接預覽中設置的對象來指定應用程序的SurfaceView 預覽layout元素。
警告: 必須按照如下順序調用MediaRecorder的下列配置方法,否則應用程序將會引發錯誤,錄像也將失敗。
c. 準備MediaRecorder —— 調用MediaRecorder.prepare()設置配置,準備好MediaRecorder 。
d. 啓動MediaRecorder —— 調用MediaRecorder.start()開始錄製視頻。
5. 停止錄製視頻 —— 按照順序調用以下方法,才能成功完成視頻錄製:
a. 停止MediaRecorder —— 調用MediaRecorder.stop()停止錄製視頻。
b. 重置MediaRecorder —— 這是可選步驟,調用MediaRecorder.reset()刪除recorder中的配置信息。
c. 釋放MediaRecorder —— 調用MediaRecorder.release()釋放MediaRecorder。
d. 鎖定攝像頭 —— 用Camera.lock()鎖定攝像頭,使得以後MediaRecorder session能夠使用它。自Android 4.0 (API level 14)開始,不再需要本調用了,除非MediaRecorder.prepare()調用失敗。
6. 停止預覽 —— activity使用完攝像頭後,應用Camera.stopPreview()停止預覽。
7. 釋放攝像頭 —— 使用Camera.release()釋放攝像頭,使其它應用程序可以使用它。
最後說下開發中遇到的比較多的問題:
1.首先來說是USB攝像頭的選擇,由於錄像所佔用的CPU資源和內存資源會相對較大,因此係統資源會顯得緊張,設備變慢運行卡頓等問題可能會在某些配置較低的設備上出現,所以要選擇的攝像頭應該儘可能適應於當前設備的配置,如果清晰度很高則會出現視頻採樣到壓縮保存過程系統無法滿足甚至是存儲設備速率無法滿足造成錄製的視頻花屏異常。
2.因此錄像需要保存,因此在保存過程中會反覆的寫存儲設備,無論是SD卡還是TF卡還是U盤,都將要求有較高的穩定性及讀寫速度,很簡單的一個例子就是我所用的TF卡最開始爲class 4的金士頓,這個過程可能因爲卡的質量不良會經常出現寫數據導致的卡內自動寫保護,進而導致了程序錄像停止甚至彈錯退出。因此錄像存儲這個地方的設備選擇需要考慮,可能換class10的卡會效果更好,但是需要考慮成本,另外也可以通過降低錄像的視頻幀率來減小錄像的大小。
3.因爲是外接USB攝像頭,所以對於USB的插拔需要隨時監聽以免出現設備已經移除了程序卻還在運行,進而導致錯誤崩潰,造成體驗不良。
4.錄像過程中的lock和unlock操作,因爲在錄像過程中無法預知會發生什麼錯誤,因此我並未採用對Camera的lock操作,因爲我所用的android設備只會外接1個Camera設備,因此不擔心多設備和多應用打開同一個攝像頭的問題,但是同樣有一個問題無法解決,就是當應用因爲未知原因出現崩潰的時候,如內存緊張造成的系統回收或者Service異常停止等等。此時的Camera就會無法得到正常的釋放,此時問題就出現了,Camera被鎖定了,其他程序無法使用,並且此應用重新打開依然無法使用。此問題現在依然未解,只能儘量的減小程序異常崩潰的概率來降低這種問題的出現了。
以上表述有些凌亂,勉強可以看看。由於Camera所涉及的東西從上到下還是有非常多的東西,而我接觸的也僅僅是其中的一小部分,僅僅是爲了工作而學習了部分有用的內容,後面如果有機會還是應該多深入的瞭解下更多的東西。最後貼出一部分調試時遇到的log打印和出處。
android\frameworks\av\services\camera\libcameraservice下的CameraService.cpp中,status_t CameraService::Client::checkPid()函數內。貼出代碼
status_t CameraService::Client::checkPid() const {
int callingPid = getCallingPid();
if (callingPid == mClientPid) return NO_ERROR;
ALOGW("attempt to use a locked camera from a different process"
" (old pid %d, new pid %d)", mClientPid, callingPid);
return EBUSY;
}
每次插拔USB設備後video的設備號會發生改變,導致無法打開攝像頭所以顯示無設備。
更新過mstar提供的最新USB的驅動後,修改瞭如下地方\kernel\drivers\media\video下的v4l2-dev.c中
<span style="font-size:14px;">void video_unregister_device(struct video_device *vdev)
{
/* Check if vdev was ever registered at all */
if (!vdev || !video_is_registered(vdev))
return;
mutex_lock(&videodev_lock);
/* This must be in a critical section to prevent a race with v4l2_open.
* Once this bit has been cleared video_get may never be called again.
*/
clear_bit(V4L2_FL_REGISTERED, &vdev->flags);
devnode_clear(vdev);//add by hzhang
mutex_unlock(&videodev_lock);
device_unregister(&vdev->dev);
}
</span>
加了一句清除設備號的代碼,使其設備號不會順序的改變,從而不會使每次打開的設備號不同而打不開設備。
E/CameraService( 855): mSurface or mPreviewWindow must be set before startRecordingMode.
查到CameraService.cpp(frameworks/av/services/camera/libcameraservice/)
status_t CameraService::Client::startCameraMode函數內
case CAMERA_RECORDING_MODE:
if (mSurface == 0 && mPreviewWindow == 0) {
ALOGE("mSurface or mPreviewWindow must be set before startRecordingMode.");
//return INVALID_OPERATION;
}
註釋掉那個返回值,使其在沒有預覽窗口View時也可以繼續開始錄像。
以上是部分log和出處,希望對有需要的同學有用。