美文网首页Android开发Android中多USB摄像头解决方案Android开发经验谈
Android中多USB摄像头解决方案——UVCCamera源码

Android中多USB摄像头解决方案——UVCCamera源码

作者: Meteorwizard | 来源:发表于2019-07-02 21:30 被阅读9次

    前言

    前段时间捣鼓多USB摄像头的方案,一阵手忙脚乱算是勉强跑起来了。整个流程主要还是依赖于网上大神们封装好的库。之前想仔细分析一下整套底层实现,然而一直拖到现在……也没有完全看完,于是想着干脆分阶段总结吧。未来打算用几篇文章的篇幅来分析启动、拍照、视频录制等几个环节。
    本篇就从相机的初始化、启动预览说起吧。废话少说,进入正题。

    先贴链接:

    1. UVCCamera:
      https://github.com/saki4510t/UVCCamera

    2. Android中多USB摄像头解决方案——UVCCamera:
      https://www.jianshu.com/p/9108ddfd0a0d

    整个UVCCamera框架包括了Java层封装,c层UVCCamera、c层libuvc以及c层libusb这几个库。

    Java层

    我们先从业务方直接可以调用的最上层(Java层)说起。
    在初始化阶段,整个Java层会涉及到的类有:

    1. com.serenegiant.usb.USBMonitor
    2. com.serenegiant.usb.USBMonitor.UsbControlBlock
    3. com.serenegiant.usb.USBMonitor.OnDeviceConnectListener
    4. com.serenegiant.usb.common.UVCCameraHandler
    5. com.serenegiant.usb.common.AbstractUVCCameraHandler.CameraThread

    稍微画了一下整个调用流程,读者可以粗略看一下有个大概印象:


    Java层时序图

    当我们启动相机的时候,第一件要做的事情就是要连接上摄像头,依然是usb摄像头,那么自然我们会需要尝试建立usb连接。而连接usb设备要做的第一件事就是获取权限:

    /**
         * 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;
        }
    

    从代码中可以看到,在获取到权限之后继而调用了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等参数。在它的构造函数里会调用USBMonitor中mUsbManager的openDevice方法来创建连接。

    /**
             * 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);
    
                ... //省略代码
    

    然后我们继续回到processConnect方法,在usb连接建立之后,会调用USBMonitor中的监听接口:mOnDeviceConnectListener,这个接口是从外部创建USBMonitor时候实现的,而在该接口的onConnect方法里我们就可以拿到usb连接建立成功的回调,在该回调里就可以调用UVCCameraHandler的open方法来准备真正启动相机。
    UVCCameraHandler是一个Handler,在其内部是通过Android的消息机制来管理整个相机的生命周期。当我们调用open方法的时候,其实是发送了一个message:

    public void open(final USBMonitor.UsbControlBlock ctrlBlock) {
            checkReleased();
            sendMessage(obtainMessage(MSG_OPEN, ctrlBlock));
        }
    

    在handleMessage中会调用创建UVCCameraHandler时候同时创建的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);
                }
            }
    

    我们可以看到,在该方法中创建了与c层交互的核心类——UVCCamera。创建完之后继而直接调用了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("调用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方法中调用了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下找到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);
    }
    

    大段大段的日文注释是不是很出戏……然而我们需要关注的是两个核心方法的调用:uvc_get_device_with_fd、uvc_open。其中uvc_get_device_with_fd方法是根据从Java层传入的vendorId和productId来寻找设备,如果找到该设备则继续调用uvc_open来开启设备。当开启成功后紧接着又做了一堆初始化工作,其中包括了创建UVCPreview类。该类封装了预览宽高、帧率、带宽、颜色格式等参数。

    我们再看nativeGetSupportedSize在C端的实现,这方法比较简单,根据方法名就能知道就是用来获取该设备支持的预览尺寸,以便后续设置使用。

    char *UVCCamera::getSupportedSize() {
        ENTER();
        if (mDeviceHandle) {
            UVCDiags params;
            RETURN(params.getSupportedSize(mDeviceHandle), char *)
        }
        RETURN(NULL, char *);
    }
    

    最后我们再来看nativeSetPreviewSize方法,这个方法的作用也很显而易见,就是在设置预览的尺寸……

    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方法。

    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方法将各参数设置给相机设备。

    小结

    本篇这个系列的第二篇(第一篇链接:https://www.jianshu.com/p/9108ddfd0a0d),对于UVCCamera的源码分析还比较粗糙,后期我将会在边学习的过程中逐渐完善一些细节,并且由于这个库创建也比较早而且后续貌似也没有在维护,因此根据网上其他人的经验会有很多问题(闪退、兼容性问题等等)希望在本次学习过程中能发现这些问题,并尝试修改。

    相关文章

      网友评论

        本文标题:Android中多USB摄像头解决方案——UVCCamera源码

        本文链接:https://www.haomeiwen.com/subject/jdzthctx.html