前言
前段時間搗鼓多USB攝像頭的方案,一陣手忙腳亂算是勉強跑起來了。整個流程主要還是依賴于網(wǎng)上大神們封裝好的庫。之前想仔細(xì)分析一下整套底層實現(xiàn),然而一直拖到現(xiàn)在……也沒有完全看完,于是想著干脆分階段總結(jié)吧。未來打算用幾篇文章的篇幅來分析啟動、拍照、視頻錄制等幾個環(huán)節(jié)。
本篇就從相機的初始化、啟動預(yù)覽說起吧。廢話少說,進入正題。
先貼鏈接:
UVCCamera:
https://github.com/saki4510t/UVCCameraAndroid中多USB攝像頭解決方案——UVCCamera:
http://m.itdecent.cn/p/9108ddfd0a0d
整個UVCCamera框架包括了Java層封裝,c層UVCCamera、c層libuvc以及c層libusb這幾個庫。
Java層
我們先從業(yè)務(wù)方直接可以調(diào)用的最上層(Java層)說起。
在初始化階段,整個Java層會涉及到的類有:
- com.serenegiant.usb.USBMonitor
- com.serenegiant.usb.USBMonitor.UsbControlBlock
- com.serenegiant.usb.USBMonitor.OnDeviceConnectListener
- com.serenegiant.usb.common.UVCCameraHandler
- com.serenegiant.usb.common.AbstractUVCCameraHandler.CameraThread
稍微畫了一下整個調(diào)用流程,讀者可以粗略看一下有個大概印象:

當(dāng)我們啟動相機的時候,第一件要做的事情就是要連接上攝像頭,依然是usb攝像頭,那么自然我們會需要嘗試建立usb連接。而連接usb設(shè)備要做的第一件事就是獲取權(quán)限:
/**
* request permission to access to USB device
* @param device
* @return true if fail to request permission
*/
public synchronized boolean requestPermission(final UsbDevice device) {
// if (DEBUG) Log.v(TAG, "requestPermission:device=" + device);
boolean result = false;
if (isRegistered()) {
if (device != null) {
if (mUsbManager.hasPermission(device)) {
// call onConnect if app already has permission
processConnect(device);
} else {
try {
// パーミッションがなければ要求する
mUsbManager.requestPermission(device, mPermissionIntent);
} catch (final Exception e) {
// Android5.1.xのGALAXY系でandroid.permission.sec.MDM_APP_MGMTという意味不明の例外生成するみたい
Log.w(TAG, e);
processCancel(device);
result = true;
}
}
} else {
processCancel(device);
result = true;
}
} else {
processCancel(device);
result = true;
}
return result;
}
從代碼中可以看到,在獲取到權(quán)限之后繼而調(diào)用了processConnect方法來嘗試建立usb連接:
/**
* open specific USB device
* @param device
*/
private final void processConnect(final UsbDevice device) {
if (destroyed) return;
updatePermission(device, true);
mAsyncHandler.post(new Runnable() {
@Override
public void run() {
if (DEBUG) Log.v(TAG, "processConnect:device=" + device);
UsbControlBlock ctrlBlock;
final boolean createNew;
ctrlBlock = mCtrlBlocks.get(device);
if (ctrlBlock == null) {
ctrlBlock = new UsbControlBlock(USBMonitor.this, device);
mCtrlBlocks.put(device, ctrlBlock);
createNew = true;
} else {
createNew = false;
}
if (mOnDeviceConnectListener != null) {
mOnDeviceConnectListener.onConnect(device, ctrlBlock, createNew);
}
}
});
}
在該方法中我們可以看到在第一次建立連接的時候會新建一個UsbControlBlock,這個類主要是用來管理USBMonitor、UsbDevice以及諸如vendorId等參數(shù)。在它的構(gòu)造函數(shù)里會調(diào)用USBMonitor中mUsbManager的openDevice方法來創(chuàng)建連接。
/**
* this class needs permission to access USB device before constructing
* @param monitor
* @param device
*/
private UsbControlBlock(final USBMonitor monitor, final UsbDevice device) {
... //省略代碼
mWeakMonitor = new WeakReference<USBMonitor>(monitor);
mWeakDevice = new WeakReference<UsbDevice>(device);
mConnection = monitor.mUsbManager.openDevice(device);
... //省略代碼
然后我們繼續(xù)回到processConnect方法,在usb連接建立之后,會調(diào)用USBMonitor中的監(jiān)聽接口:mOnDeviceConnectListener,這個接口是從外部創(chuàng)建USBMonitor時候?qū)崿F(xiàn)的,而在該接口的onConnect方法里我們就可以拿到usb連接建立成功的回調(diào),在該回調(diào)里就可以調(diào)用UVCCameraHandler的open方法來準(zhǔn)備真正啟動相機。
UVCCameraHandler是一個Handler,在其內(nèi)部是通過Android的消息機制來管理整個相機的生命周期。當(dāng)我們調(diào)用open方法的時候,其實是發(fā)送了一個message:
public void open(final USBMonitor.UsbControlBlock ctrlBlock) {
checkReleased();
sendMessage(obtainMessage(MSG_OPEN, ctrlBlock));
}
在handleMessage中會調(diào)用創(chuàng)建UVCCameraHandler時候同時創(chuàng)建的CameraThread的handleOpen方法。
public void handleOpen(final USBMonitor.UsbControlBlock ctrlBlock) {
handleClose();
try {
final UVCCamera camera = new UVCCamera();
camera.open(ctrlBlock);
synchronized (mSync) {
mUVCCamera = camera;
}
callOnOpen();
} catch (final Exception e) {
callOnError(e);
}
}
我們可以看到,在該方法中創(chuàng)建了與c層交互的核心類——UVCCamera。創(chuàng)建完之后繼而直接調(diào)用了open方法。
/**
* connect to a UVC camera
* USB permission is necessary before this method is called
* @param ctrlBlock
*/
public synchronized void open(final UsbControlBlock ctrlBlock) {
int result = -2;
StringBuilder sb = new StringBuilder();
try {
mCtrlBlock = ctrlBlock.clone();
result = nativeConnect(mNativePtr,
mCtrlBlock.getVenderId(), mCtrlBlock.getProductId(),
mCtrlBlock.getFileDescriptor(),
mCtrlBlock.getBusNum(),
mCtrlBlock.getDevNum(),
getUSBFSName(mCtrlBlock));
sb.append("調(diào)用nativeConnect返回值:"+result);
// long id_camera, int venderId, int productId, int fileDescriptor, int busNum, int devAddr, String usbfs
} catch (final Exception e) {
Log.w(TAG, e);
for(int i = 0; i< e.getStackTrace().length; i++){
sb.append(e.getStackTrace()[i].toString());
sb.append("\n");
}
sb.append("core message ->"+e.getLocalizedMessage());
result = -1;
}
if (result != 0) {
throw new UnsupportedOperationException("open failed:result=" + result+"----->" +
"id_camera="+mNativePtr+";venderId="+mCtrlBlock.getVenderId()
+";productId="+mCtrlBlock.getProductId()+";fileDescriptor="+mCtrlBlock.getFileDescriptor()
+";busNum="+mCtrlBlock.getBusNum()+";devAddr="+mCtrlBlock.getDevNum()
+";usbfs="+getUSBFSName(mCtrlBlock)+"\n"+"Exception:"+sb.toString());
}
if (mNativePtr != 0 && TextUtils.isEmpty(mSupportedSize)) {
mSupportedSize = nativeGetSupportedSize(mNativePtr);
}
nativeSetPreviewSize(mNativePtr, DEFAULT_PREVIEW_WIDTH, DEFAULT_PREVIEW_HEIGHT,
DEFAULT_PREVIEW_MIN_FPS, DEFAULT_PREVIEW_MAX_FPS, DEFAULT_PREVIEW_MODE, DEFAULT_BANDWIDTH);
}
可以看到UVCCamera的open方法中調(diào)用了nativeConnect、nativeGetSupportedSize、nativeSetPreviewSize 這三個native的方法來真正啟動相機。
相機啟動之后會繼續(xù)回到CameraThread的handleOpen方法,在該方法中又調(diào)用了callOnOpen來通知外部相機開啟繼而完成整個相機的啟動過程。
C層
我們接著上面來繼續(xù)分析c層的調(diào)用。Java層中UVCCamera的nativeConnect、nativeGetSupportedSize、nativeSetPreviewSize三個native方法具體實現(xiàn)是在libUVCCamera.so中。從GitHub上clone下來UVCCamera完整的代碼之后,我們可以找到UVCCamera/libuvccamera/src/main/jni/UVCCamera/serenegiant_usb_UVCCamera.cpp這個類,Java層調(diào)用的nativeXXX方法就是在該類中封裝的,而serenegiant_usb_UVCCamera實際調(diào)用的是UVCCamera/libuvccamera/src/main/jni/UVCCamera/UVCCamera.cpp。繼而可以在該類中找到connect方法。
//======================================================================
/**
* カメラへ接続する
*/
int UVCCamera::connect(int vid, int pid, int fd, int busnum, int devaddr, const char *usbfs) {
ENTER();
uvc_error_t result = UVC_ERROR_BUSY;
if (!mDeviceHandle && fd) {
if (mUsbFs)
free(mUsbFs);
mUsbFs = strdup(usbfs);
if (UNLIKELY(!mContext)) {
result = uvc_init2(&mContext, NULL, mUsbFs);
// libusb_set_debug(mContext->usb_ctx, LIBUSB_LOG_LEVEL_DEBUG);
if (UNLIKELY(result < 0)) {
LOGD("failed to init libuvc");
RETURN(result, int);
}
}
// カメラ機能フラグをクリア
clearCameraParams();
fd = dup(fd);
// 指定したvid,idを持つデバイスを検索, 見つかれば0を返してmDeviceに見つかったデバイスをセットする(既に1回uvc_ref_deviceを呼んである)
// result = uvc_find_device2(mContext, &mDevice, vid, pid, NULL, fd);
result = uvc_get_device_with_fd(mContext, &mDevice, vid, pid, NULL, fd, busnum, devaddr);
if (LIKELY(!result)) {
// カメラのopen処理
result = uvc_open(mDevice, &mDeviceHandle);
if (LIKELY(!result)) {
// open出來た時
#if LOCAL_DEBUG
uvc_print_diag(mDeviceHandle, stderr);
#endif
mFd = fd;
mStatusCallback = new UVCStatusCallback(mDeviceHandle);
mButtonCallback = new UVCButtonCallback(mDeviceHandle);
mPreview = new UVCPreview(mDeviceHandle);
} else {
// open出來なかった時
LOGE("could not open camera:err=%d", result);
uvc_unref_device(mDevice);
// SAFE_DELETE(mDevice); // 參照カウンタが0ならuvc_unref_deviceでmDeviceがfreeされるから不要 XXX クラッシュ, 既に破棄されているのを再度破棄しようとしたからみたい
mDevice = NULL;
mDeviceHandle = NULL;
close(fd);
}
} else {
LOGE("could not find camera:err=%d", result);
close(fd);
}
} else {
// カメラが既にopenしている時
LOGW("camera is already opened. you should release first");
}
RETURN(result, int);
}
大段大段的日文注釋是不是很出戲……然而我們需要關(guān)注的是兩個核心方法的調(diào)用:uvc_get_device_with_fd、uvc_open。其中uvc_get_device_with_fd方法是根據(jù)從Java層傳入的vendorId和productId來尋找設(shè)備,如果找到該設(shè)備則繼續(xù)調(diào)用uvc_open來開啟設(shè)備。當(dāng)開啟成功后緊接著又做了一堆初始化工作,其中包括了創(chuàng)建UVCPreview類。該類封裝了預(yù)覽寬高、幀率、帶寬、顏色格式等參數(shù)。
我們再看nativeGetSupportedSize在C端的實現(xiàn),這方法比較簡單,根據(jù)方法名就能知道就是用來獲取該設(shè)備支持的預(yù)覽尺寸,以便后續(xù)設(shè)置使用。
char *UVCCamera::getSupportedSize() {
ENTER();
if (mDeviceHandle) {
UVCDiags params;
RETURN(params.getSupportedSize(mDeviceHandle), char *)
}
RETURN(NULL, char *);
}
最后我們再來看nativeSetPreviewSize方法,這個方法的作用也很顯而易見,就是在設(shè)置預(yù)覽的尺寸……
int UVCCamera::setPreviewSize(int width, int height, int min_fps, int max_fps, int mode, float bandwidth) {
ENTER();
int result = EXIT_FAILURE;
if (mPreview) {
result = mPreview->setPreviewSize(width, height, min_fps, max_fps, mode, bandwidth);
}
RETURN(result, int);
}
可以看到這邊其實是調(diào)用了UVCPreview的setPreviewSize方法。
int UVCPreview::setPreviewSize(int width, int height, int min_fps, int max_fps, int mode, float bandwidth) {
ENTER();
int result = 0;
if ((requestWidth != width) || (requestHeight != height) || (requestMode != mode)) {
requestWidth = width;
requestHeight = height;
requestMinFps = min_fps;
requestMaxFps = max_fps;
requestMode = mode;
requestBandwidth = bandwidth;
uvc_stream_ctrl_t ctrl;
result = uvc_get_stream_ctrl_format_size_fps(mDeviceHandle, &ctrl,
!requestMode ? UVC_FRAME_FORMAT_YUYV : UVC_FRAME_FORMAT_MJPEG,
requestWidth, requestHeight, requestMinFps, requestMaxFps);
}
RETURN(result, int);
}
在該方法中最終是調(diào)用了uvc_get_stream_ctrl_format_size_fps方法將各參數(shù)設(shè)置給相機設(shè)備。
當(dāng)相機的open流程走完之后,只是代表了初始化工作的完成,但還未真正開啟預(yù)覽。而預(yù)覽的動作是在USBMonitor.OnDeviceConnectListener的onConnect回調(diào)中執(zhí)行openCamera之后進行的。下一篇文章將會分析startPreview的一系列動作。
小結(jié)
本篇這個系列的第二篇(第一篇鏈接:http://m.itdecent.cn/p/9108ddfd0a0d),對于UVCCamera的源碼分析還比較粗糙,后期我將會在邊學(xué)習(xí)的過程中逐漸完善一些細(xì)節(jié),并且由于這個庫創(chuàng)建也比較早而且后續(xù)貌似也沒有在維護,因此根據(jù)網(wǎng)上其他人的經(jīng)驗會有很多問題(閃退、兼容性問題等等)希望在本次學(xué)習(xí)過程中能發(fā)現(xiàn)這些問題,并嘗試修改。