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

Java层

接着上一篇文章(https://www.jianshu.com/p/f7f548c2c0e7
)的分析。
在成功调用UVCCamera的一系列open操作之后,我们就可以进入startPreview阶段。这个阶段的上层调用逻辑相对比较简单,我们先看一下一个大概的时序图:

Java层时序图

我们在USBMonitor.OnDeviceConnectListener的onConnect回调中(openCamra之后)先调用了UVCCameraHandler的startPreview方法。
查看源码发现UVCCameraHandler中实际是调用了super的startPreview,那么我们再看到super也就是AbstractUVCCameraHandler中:

//UVCCameraHandler
@Override
public void startPreview(final Object surface) {
    super.startPreview(surface);
}
//AbstractUVCCameraHandler
public void startPreview(final Object surface) {
        checkReleased();
        if (!((surface instanceof SurfaceHolder) || (surface instanceof Surface) || (surface instanceof SurfaceTexture))) {
            throw new IllegalArgumentException("surface should be one of SurfaceHolder, Surface or SurfaceTexture: " + surface);
        }

        sendMessage(obtainMessage(MSG_PREVIEW_START, surface));
    }

相机使用的整个流程大致可以分为“数据采集”+“渲染”,而startPreview就是这两个过程结合之处,我们可以看到startPreview需要传入一个类型为Object的surface,这个就是我们渲染所需要的纹理,相机采集到的每一帧数据都需要绘制到这块纹理上,后面实现拍照、视频录制甚至是图像编辑(裁剪、滤镜等)都需要与这块纹理打交道,我们将在后续讲到“渲染”流程的时候再详细介绍。这里我们只需要知道从外部传入一块纹理即可。在Android UI中对于纹理的封装就是SurfaceView或者TextureView,而在UVCCamera中就是UVCCameraTextureView,它继承自android.view.TextureView。

我们继续回到预览流程,可以看到AbstractUVCCameraHandler中拿到外部传入的surface之后,执行了与openCamera相同的流程——通过消息机制调度相机操作,这里就发送了一个MSG_PREVIEW_START消息通知AbstractUVCCameraHandler.CameraThread来开启预览。

//AbstractUVCCameraHandler
 case MSG_PREVIEW_START:
    thread.handleStartPreview(msg.obj);
public void handleStartPreview(final Object surface) {
            if (DEBUG) Log.v(TAG_THREAD, "handleStartPreview:");
            if ((mUVCCamera == null) || mIsPreviewing) return;
            try {
                mUVCCamera.setPreviewSize(mWidth, mHeight, 1, 31, mPreviewMode, mBandwidthFactor);
//              mUVCCamera.setFrameCallback(mIFrameCallback, UVCCamera.PIXEL_FORMAT_NV21);
                mUVCCamera.setFrameCallback(mIFrameCallback, UVCCamera.PIXEL_FORMAT_YUV420SP);
            } catch (final IllegalArgumentException e) {
                try {
                    // fallback to YUV mode
                    mUVCCamera.setPreviewSize(mWidth, mHeight, 1, 31, UVCCamera.DEFAULT_PREVIEW_MODE, mBandwidthFactor);
                } catch (final IllegalArgumentException e1) {
                    callOnError(e1);
                    return;
                }
            }
            if (surface instanceof SurfaceHolder) {
                mUVCCamera.setPreviewDisplay((SurfaceHolder) surface);
            }
            if (surface instanceof Surface) {
                mUVCCamera.setPreviewDisplay((Surface) surface);
            } else {
                mUVCCamera.setPreviewTexture((SurfaceTexture) surface);
            }
            mUVCCamera.startPreview();
            mUVCCamera.updateCameraParams();
            synchronized (mSync) {
                mIsPreviewing = true;
            }
            callOnStartPreview();
        }

CameraThread中的handleStartPreview大概就是做了设置预览相关参数、设置预览数据回调监听、绑定纹理、开启预览等这几部操作。根据代码中方法的名字基本也能了解整个调用的逻辑。之后的代码就进入到了jni层。我们一个个来分析。

C层

  1. setPreviewSize
    在Java层的UVCCamera中,该方法实际调用了jni的方法——nativeSetPreviewSize。在上一篇文章中,我们发现在UVCCamera的open方法中也调用了nativeSetPreviewSize。nativeSetPreviewSize是用来设置预览所需要的一些相关参数的,如宽、高、fps等等。而在open方法中调用时,只是为这些参数赋了默认值,在setPreviewSize中则是从外部传入的上层业务中实际的值。至于c层代码也没什么好说的,就是一通赋值操作
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);
}

  1. setFrameCallback
    在Java层的UVCCamera中,该方法传入的第一个参数是IFrameCallback接口,能让我们拿到相机采集的每一帧原始数据。另外的一个参数则是指定色彩格式,这边我们使用的是YUV420。我们在使用usb摄像头过程中如果出现一些色彩不正确的问题,可以尝试调整这个参数,如换成 NV21。而对于色彩格式的详细介绍,网上随便一搜就会有很多,这里就不展开了。

再转到C层代码,这边调用的是nativeSetPreviewSize

int UVCPreview::setFrameCallback(JNIEnv *env, jobject frame_callback_obj, int pixel_format) {
    
        //... 省略代码
        if (!env->IsSameObject(mFrameCallbackObj, frame_callback_obj))  {
            iframecallback_fields.onFrame = NULL;
            if (mFrameCallbackObj) {
                env->DeleteGlobalRef(mFrameCallbackObj);
            }
            mFrameCallbackObj = frame_callback_obj;
            if (frame_callback_obj) {
                // get method IDs of Java object for callback
                jclass clazz = env->GetObjectClass(frame_callback_obj);
                if (LIKELY(clazz)) {
                    iframecallback_fields.onFrame = env->GetMethodID(clazz,
                        "onFrame",  "(Ljava/nio/ByteBuffer;)V");
                } else {
                    LOGW("failed to get object class");
                }
            //··· 省略代码
        }
        if (frame_callback_obj) {
            mPixelFormat = pixel_format;
            callbackPixelFormatChanged();
        }
    }
    //··· 省略代码
}

可以看到这个方法中,Java层的IFrameCallback赋值给了C层的mFrameCallbackObj。并且通过env->GetObjectClass以及env->GetMethodID将该接口的onFrame方法绑定到iframecallback_fields.onFrame。便于后续调用。在代码的最后又调用了callbackPixelFormatChanged,该方法会根据传入的色彩格式来申请对应的内存空间。

  1. setPreviewDisplay
    该方法调用流程是:
  • Java层:UVCCamera.startPreview
  • C层:serenegiant_usb_UVCCamera->nativeSetPreviewDisplay
  • C层:UVCCamera->setPreviewDisplay
  • C层:UVCPreview->setPreviewDisplay
    我们直接看UVCPreview类中的:
int UVCPreview::setPreviewDisplay(ANativeWindow *preview_window) {
    ENTER();
    pthread_mutex_lock(&preview_mutex);
    {
        if (mPreviewWindow != preview_window) {
            if (mPreviewWindow)
                ANativeWindow_release(mPreviewWindow);
            mPreviewWindow = preview_window;
            if (LIKELY(mPreviewWindow)) {
                ANativeWindow_setBuffersGeometry(mPreviewWindow,
                    frameWidth, frameHeight, previewFormat);
            }
        }
    }
    pthread_mutex_unlock(&preview_mutex);
    RETURN(0, int);
}

这个方法中,传入的ANativeWindow就是我们之前提到的纹理,转回Java层就是UVC库里封装的UVCCameraTextureView。然后这个方法主要作用就是将传入的纹理保存到mPreviewWindow这个全局变量中。之后有调用了ANativeWindow_setBuffersGeometry方法设置缓冲区的宽高,这个方法在安卓的文档就就介绍得比较详细了:
Change the format and size of the window buffers.

The width and height control the number of pixels in the buffers, not the dimensions of the window on screen. If these are different than the window's physical size, then its buffer will be scaled to match that size when compositing it to the screen. The width and height must be either both zero or both non-zero.

For all of these parameters, if 0 is supplied then the window's base value will come back in force.
翻译过来大概就是:
更改窗口缓冲区的颜色格式和尺寸。
宽度和高度控制缓冲区中的像素,而不是屏幕上窗口的尺寸。如果这些大小与窗口的物理大小不同,则在将其合成到屏幕时,将缩放其缓冲区以匹配该大小。宽度和高度必须同时为零或同时为非零。
对于所有这些参数,如果设置为0,则窗口的默认值将重新生效。

小结

在做完一堆参数设置之后,接下来会调用startPreview,来真正开启预览,这个过程相对比较复杂,我们将放到下一篇中在仔细介绍

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

推荐阅读更多精彩内容