Android中多USB摄像头解决方案——UVCCamera源码分析(一)

前言

前段时间捣鼓多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/serenegiant_usb_UVCCamera.cpp这个类,Java层调用的nativeXXX方法就是在该类中封装的,而serenegiant_usb_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方法将各参数设置给相机设备。
当相机的open流程走完之后,只是代表了初始化工作的完成,但还未真正开启预览。而预览的动作是在USBMonitor.OnDeviceConnectListener的onConnect回调中执行openCamera之后进行的。下一篇文章将会分析startPreview的一系列动作。

小结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 先贴上采用的开源库链接:https://github.com/saki4510t/UVCCamera 业余时间捣鼓...
    Meteorwizard阅读 51,013评论 25 18
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 一、简历准备 1、个人技能 (1)自定义控件、UI设计、常用动画特效 自定义控件 ①为什么要自定义控件? Andr...
    lucas777阅读 5,126评论 2 54
  • 在人生的旅途中,我们经历过从无知到成熟,从孩子到大人,也做过不少傻事,曾以为父母给我一片天地,就是最好的,现在想想...
    卡夫卡ios阅读 283评论 0 3
  • 初一要练胸,大胸袪灾凶; 初二要练背,厚背霉运褪; 初三要练腿,猛腿镇小鬼; 初四要练肩,宽肩运不偏; 初五要练臂...
    門外悍阅读 360评论 0 0