前言 前段时间捣鼓多USB摄像头的方案,一阵手忙脚乱算是勉强跑起来了。整个流程主要还是依赖于网上大神们封装好的库。之前想仔细分析一下整套底层实现,然而一直拖到现在……也没有完全看完,于是想着干脆分阶段总结吧。未来打算用几篇文章的篇幅来分析启动、拍照、视频录制等几个环节。
本篇就从相机的初始化、启动预览说起吧。废话少说,进入正题。
先贴链接:
UVCCamera:https://github.com/saki4510t/UVCCamera
Android中多USB摄像头解决方案——UVCCamera:https://zsyyblog.com/97f2149a.html
整个UVCCamera框架包括了Java层封装,c层UVCCamera、c层libuvc以及c层libusb这几个库。
Java层 我们先从业务方直接可以调用的最上层(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
稍微画了一下整个调用流程,读者可以粗略看一下有个大概印象:
当我们启动相机的时候,第一件要做的事情就是要连接上摄像头,依然是usb摄像头,那么自然我们会需要尝试建立usb连接。而连接usb设备要做的第一件事就是获取权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public synchronized boolean requestPermission (final UsbDevice device) { boolean result = false ; if (isRegistered()) { if (device != null ) { if (mUsbManager.hasPermission(device)) { processConnect(device); } else { try { mUsbManager.requestPermission(device, mPermissionIntent); } catch (final Exception e) { Log.w(TAG, e); processCancel(device); result = true ; } } } else { processCancel(device); result = true ; } } else { processCancel(device); result = true ; } return result; }
从代码中可以看到,在获取到权限之后继而调用了processConnect
方法来尝试建立usb连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 等参数。在它的构造函数里会调用USBMonitor 中mUsbManager
的openDevice
方法来创建连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private UsbControlBlock (final USBMonitor monitor, final UsbDevice device) { ... mWeakMonitor = new WeakReference <USBMonitor>(monitor); mWeakDevice = new WeakReference <UsbDevice>(device); mConnection = monitor.mUsbManager.openDevice(device); ... }
然后我们继续回到processConnect
方法,在usb连接建立之后,会调用USBMonitor 中的监听接口:mOnDeviceConnectListener
,这个接口是从外部创建USBMonitor 时候实现的,而在该接口的onConnect
方法里我们就可以拿到usb连接建立成功的回调,在该回调里就可以调用UVCCameraHandler 的open
方法来准备真正启动相机。
UVCCameraHandler 是一个Handler
,在其内部是通过Android的消息机制来管理整个相机的生命周期。当我们调用open方法的时候,其实是发送了一个message:
1 2 3 4 public void open (final USBMonitor.UsbControlBlock ctrlBlock) { checkReleased(); sendMessage(obtainMessage(MSG_OPEN, ctrlBlock)); }
在handleMessage
中会调用创建UVCCameraHandler 时候同时创建的CameraThread 的handleOpen
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 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); } }
我们可以看到,在该方法中创建了与c层交互的核心类——UVCCamera 。创建完之后继而直接调用了open方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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("调用nativeConnect返回值:" +result); } 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
方法中调用了nativeConnect
、nativeGetSupportedSize
、nativeSetPreviewSize
这三个native的方法来真正启动相机。 相机启动之后会继续回到CameraThread 的handleOpen
方法,在该方法中又调用了callOnOpen
来通知外部相机开启继而完成整个相机的启动过程。
C层 我们接着上面来继续分析c层的调用。Java层中UVCCamera的nativeConnect
、nativeGetSupportedSize
、nativeSetPreviewSize
三个native方法具体实现是在libUVCCamera.so
中。从GitHub上clone下来UVCCamera 完整的代码之后,我们可以找到UVCCamera/libuvccamera/src/main/jni/UVCCamera/serenegiant_usb_UVCCamera.cpp
这个类,Java层调用的nativeXXX
方法就是在该类中封装的,而serenegiant_usb_UVCCamera
实际调用的是UVCCamera/libuvccamera/src/main/jni/UVCCamera/UVCCamera.cpp
。继而可以在该类中找到connect方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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); if (UNLIKELY (result < 0 )) { LOGD ("failed to init libuvc" ); RETURN (result, int ); } } clearCameraParams (); fd = dup (fd); result = uvc_get_device_with_fd (mContext, &mDevice, vid, pid, NULL , fd, busnum, devaddr); if (LIKELY (!result)) { result = uvc_open (mDevice, &mDeviceHandle); if (LIKELY (!result)) { #if LOCAL_DEBUG uvc_print_diag (mDeviceHandle, stderr); #endif mFd = fd; mStatusCallback = new UVCStatusCallback (mDeviceHandle); mButtonCallback = new UVCButtonCallback (mDeviceHandle); mPreview = new UVCPreview (mDeviceHandle); } else { LOGE ("could not open camera:err=%d" , result); uvc_unref_device (mDevice); mDevice = NULL ; mDeviceHandle = NULL ; close (fd); } } else { LOGE ("could not find camera:err=%d" , result); close (fd); } } else { LOGW ("camera is already opened. you should release first" ); } RETURN (result, int ); }
我们需要关注的是两个核心方法的调用:uvc_get_device_with_fd
、uvc_open
。其中uvc_get_device_with_fd
方法是根据从Java层传入的vendorId
和productId
来寻找设备,如果找到该设备则继续调用uvc_open
来开启设备。当开启成功后紧接着又做了一堆初始化工作,其中包括了创建UVCPreview类。该类封装了预览宽高、帧率、带宽、颜色格式等参数。
我们再看nativeGetSupportedSize
在C端的实现,这方法比较简单,根据方法名就能知道就是用来获取该设备支持的预览尺寸,以便后续设置使用。
1 2 3 4 5 6 7 8 char *UVCCamera::getSupportedSize () { ENTER (); if (mDeviceHandle) { UVCDiags params; RETURN (params.getSupportedSize (mDeviceHandle), char *) } RETURN (NULL , char *); }
最后我们再来看nativeSetPreviewSize
方法,这个方法的作用也很显而易见,就是在设置预览的尺寸……
1 2 3 4 5 6 7 8 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 ); }
可以看到这边其实是调用了UVCPreview 的setPreviewSize
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ); }
在该方法中最终是调用了uvc_get_stream_ctrl_format_size_fps
方法将各参数设置给相机设备。
当相机的open流程走完之后,只是代表了初始化工作的完成,但还未真正开启预览。而预览的动作是在USBMonitor.OnDeviceConnectListener
的onConnect
回调中执行openCamera之后进行的。下一篇文章将会分析startPreview
的一系列动作。
小结 本篇这个系列的第二篇(第一篇链接:https://zsyyblog.com/97f2149a.html ),对于UVCCamera的源码分析还比较粗糙,后期我将会在边学习的过程中逐渐完善一些细节,并且由于这个库创建也比较早而且后续貌似也没有在维护,因此根据网上其他人的经验会有很多问题(闪退、兼容性问题等等)希望在本次学习过程中能发现这些问题,并尝试修改。
相关内容
参考链接:https://www.jianshu.com/p/f7f548c2c0e7