Java层

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

Java层时序图

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

1
2
3
4
5
//UVCCameraHandler
@Override
public void startPreview(final Object surface) {
super.startPreview(surface);
}
1
2
3
4
5
6
7
8
9
//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来开启预览。

1
2
3
//AbstractUVCCameraHandler
case MSG_PREVIEW_START:
thread.handleStartPreview(msg.obj);
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
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方法中也调用了nativeSetPreviewSizenativeSetPreviewSize是用来设置预览所需要的一些相关参数的,如宽、高、fps等等。而在open方法中调用时,只是为这些参数赋了默认值,在setPreviewSize中则是从外部传入的上层业务中实际的值。至于c层代码也没什么好说的,就是一通赋值操作
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);
}
  1. setFrameCallback
    在Java层的UVCCamera中,该方法传入的第一个参数是IFrameCallback接口,能让我们拿到相机采集的每一帧原始数据。另外的一个参数则是指定色彩格式,这边我们使用的是YUV420。我们在使用usb摄像头过程中如果出现一些色彩不正确的问题,可以尝试调整这个参数,如换成 NV21。而对于色彩格式的详细介绍,网上随便一搜就会有很多,这里就不展开了。

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

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
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类中的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,来真正开启预览,这个过程相对比较复杂,我们将放到下一篇中在仔细介绍

相关内容

参考链接:https://www.jianshu.com/p/225734c143ba