Android中多USB攝像頭解決方案——UVCCamera源碼分析(一)

前言

前段時間搗鼓多USB攝像頭的方案,一陣手忙腳亂算是勉強跑起來了。整個流程主要還是依賴于網(wǎng)上大神們封裝好的庫。之前想仔細(xì)分析一下整套底層實現(xiàn),然而一直拖到現(xiàn)在……也沒有完全看完,于是想著干脆分階段總結(jié)吧。未來打算用幾篇文章的篇幅來分析啟動、拍照、視頻錄制等幾個環(huán)節(jié)。
本篇就從相機的初始化、啟動預(yù)覽說起吧。廢話少說,進入正題。

先貼鏈接:

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

  2. Android中多USB攝像頭解決方案——UVCCamera:
    http://www.itdecent.cn/p/9108ddfd0a0d

整個UVCCamera框架包括了Java層封裝,c層UVCCamera、c層libuvc以及c層libusb這幾個庫。

Java層

我們先從業(yè)務(wù)方直接可以調(diào)用的最上層(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

稍微畫了一下整個調(diào)用流程,讀者可以粗略看一下有個大概印象:


Java層時序圖

當(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://www.itdecent.cn/p/9108ddfd0a0d),對于UVCCamera的源碼分析還比較粗糙,后期我將會在邊學(xué)習(xí)的過程中逐漸完善一些細(xì)節(jié),并且由于這個庫創(chuàng)建也比較早而且后續(xù)貌似也沒有在維護,因此根據(jù)網(wǎng)上其他人的經(jīng)驗會有很多問題(閃退、兼容性問題等等)希望在本次學(xué)習(xí)過程中能發(fā)現(xiàn)這些問題,并嘗試修改。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 先貼上采用的開源庫鏈接:https://github.com/saki4510t/UVCCamera 業(yè)余時間搗鼓...
    Meteorwizard閱讀 54,832評論 25 18
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,684評論 1 32
  • 一、簡歷準(zhǔn)備 1、個人技能 (1)自定義控件、UI設(shè)計、常用動畫特效 自定義控件 ①為什么要自定義控件? Andr...
    lucas777閱讀 5,394評論 2 54
  • 在人生的旅途中,我們經(jīng)歷過從無知到成熟,從孩子到大人,也做過不少傻事,曾以為父母給我一片天地,就是最好的,現(xiàn)在想想...
    卡夫卡ios閱讀 339評論 0 3
  • 初一要練胸,大胸袪災(zāi)兇; 初二要練背,厚背霉運褪; 初三要練腿,猛腿鎮(zhèn)小鬼; 初四要練肩,寬肩運不偏; 初五要練臂...
    門外悍閱讀 460評論 0 0

友情鏈接更多精彩內(nèi)容