NDK Camera2 API를 사용하여 안드로이드 QR 스캐너를 구현하는 방법

74001 단어 androidqrndkcamera2
안드로이드 개발자로서 메모리 복사와 쓰레기 수집을 줄이고 싶다면 자바/Kotlin에서 C++로 코드를 옮길 수 있다.본문에서, 나는 QR 스캐너를 예로 들었다.나는 NDK camera2 API를 사용하여 카메라 미리보기를 실현하고 Dynamsoft Mobile Barcode SDK를 사용하여 C++에서 QR코드를 디코딩할 것이다.

예비 지식


  • Dynamsoft Barcode SDK for Android v8.8
    SDK를 활성화하려면 valid license가 필요합니다.
  • 최신 Android Studio
  • NDK 22.1.71670
  • Dynamsoft Android SDK의 C++ API 정보


    Dynamsoft는 안드로이드 개발에 AAR 패키지를 제공합니다.우리는 libDynamsoftBarcodeReaderAndroid.so에서 C++ 공유 라이브러리DynamsoftBarcodeReaderAndroid.aar를 추출할 수 있다.헤더 파일은 Dynamsoft C++ SDK 가방에서 얻을 수 있습니다.

    Android NDK Camera2 샘플 시작


    NDK 샘플 라이브러리는 NDK 개발을 위한 속성 강좌를 제공합니다.한번 봅시다camera2 samples.
    camera 폴더에는 다음 두 가지 예가 있습니다.
  • Basic: 네이티브 활동을 만들고 네이티브 창에 카메라 프레임을 그립니다.
  • 텍스쳐 뷰: 텍스쳐 뷰에 카메라 프레임을 그립니다.
  • 본 컴퓨터 활동에서 어떻게 카메라 미리보기를 실현합니까


  • 자바로 확장Activity된 안드로이드NativeActivity를 만듭니다.
    public class CameraActivity extends NativeActivity {}
    

  • 네이티브 활동의 JNI 엔트리 점을 작성하려면 다음과 같이 하십시오.
    extern "C" void android_main(struct android_app* state) {
      CameraEngine engine(state);
      pEngineObj = &engine;
    
      state->userData = reinterpret_cast<void*>(&engine);
      state->onAppCmd = ProcessAndroidCmd;
    
      // loop waiting for stuff to do.
      while (1) {
        // Read all pending events.
        int events;
        struct android_poll_source* source;
    
        while (ALooper_pollAll(0, NULL, &events, (void**)&source) >= 0) {
          // Process this event.
          if (source != NULL) {
            source->process(state, source);
          }
    
          // Check if we are exiting.
          if (state->destroyRequested != 0) {
            LOGI("CameraEngine thread destroy requested!");
            engine.DeleteCamera();
            pEngineObj = nullptr;
            return;
          }
        }
        pEngineObj->DrawFrame();
      }
    }
    

  • 무한 순환에서 카메라 미리보기를 그립니다. 이 순환은 계속 호출됩니다. pEngineObj->DrawFrame()ImageReader 카메라 미리보기 프레임을 가져올 수 있습니다.
    void CameraEngine::DrawFrame(void) {
      if (!cameraReady_ || !yuvReader_) return;
      AImage* image = yuvReader_->GetNextImage();
      if (!image) {
        return;
      }
    
      ANativeWindow_acquire(app_->window);
      ANativeWindow_Buffer buf;
      if (ANativeWindow_lock(app_->window, &buf, nullptr) < 0) {
        yuvReader_->DeleteImage(image);
        return;
      }
    
      yuvReader_->DisplayImage(&buf, image);
      ANativeWindow_unlockAndPost(app_->window);
      ANativeWindow_release(app_->window);
    }
    
  • Android TextureView 예


    이 예에서는 먼저 Java를 사용하여 텍스쳐 뷰를 생성합니다.
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:layout_centerHorizontal="true"
                 android:layout_centerVertical="true">
        <TextureView
            android:id="@+id/texturePreview"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
    </FrameLayout>
    
    private void createTextureView() {
        textureView_ = (TextureView) findViewById(R.id.texturePreview);
        textureView_.setSurfaceTextureListener(this);
        if (textureView_.isAvailable()) {
            onSurfaceTextureAvailable(textureView_.getSurfaceTexture(),
                    textureView_.getWidth(), textureView_.getHeight());
        }
    }
    
    public void onSurfaceTextureAvailable(SurfaceTexture surface,
                                            int width, int height) {
        createNativeCamera();
    
        resizeTextureView(width, height);
        surface.setDefaultBufferSize(cameraPreviewSize_.getWidth(),
                cameraPreviewSize_.getHeight());
        surface_ = new Surface(surface);
        onPreviewSurfaceCreated(ndkCamera_, surface_);
        scheduleTask();
    }
    
    무늬 보기를 초기화할 때, 그 곡면은 본 컴퓨터 코드에 연결된 카메라의 출력에 귀속됩니다.
    extern "C" JNIEXPORT void JNICALL
    Java_com_sample_textureview_ViewActivity_onPreviewSurfaceCreated(
        JNIEnv *env, jobject instance, jlong ndkCameraObj, jobject surface) {
      ASSERT(ndkCameraObj && (jlong)pEngineObj == ndkCameraObj,
             "NativeObject should not be null Pointer");
      CameraAppEngine *pApp = reinterpret_cast<CameraAppEngine *>(ndkCameraObj);
      pApp->CreateCameraSession(surface);
      pApp->StartPreview(true);
    }
    
    camera_->CreateSession(ANativeWindow_fromSurface(env_, surface));
    
    void NDKCamera::CreateSession(ANativeWindow* previewWindow) {
      requests_[PREVIEW_REQUEST_IDX].outputNativeWindow_ = previewWindow;
      requests_[PREVIEW_REQUEST_IDX].template_ = TEMPLATE_PREVIEW;
    
      CALL_CONTAINER(create(&outputContainer_));
      for (auto& req : requests_) {
        if (!req.outputNativeWindow_) continue;
    
        ANativeWindow_acquire(req.outputNativeWindow_);
        CALL_OUTPUT(create(req.outputNativeWindow_, &req.sessionOutput_));
        CALL_CONTAINER(add(outputContainer_, req.sessionOutput_));
        CALL_TARGET(create(req.outputNativeWindow_, &req.target_));
        CALL_DEV(createCaptureRequest(cameras_[activeCameraId_].device_,
                                      req.template_, &req.request_));
        CALL_REQUEST(addTarget(req.request_, req.target_));
      }
    
      captureSessionState_ = CaptureSessionState::READY;
      CALL_DEV(createCaptureSession(cameras_[activeCameraId_].device_,
                                    outputContainer_, GetSessionListener(),
                                    &captureSession_));
    }
    

    TextureView 및 ImageReader 결합


    QR 스캐너를 실현하기 위해 TextureView를 사용하여 카메라 미리보기를 표시하고 ImageReader를 사용하여 카메라 프레임을 가져오고 디코딩합니다.어떻게 동시에 TextureViewImageReader로 카메라 프레임을 출력합니까?
    Java 코드를 살펴보겠습니다.
    mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                        new CameraCaptureSession.StateCallback() {
    
                            @Override
                            public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                                // The camera is already closed
                                if (null == mCameraDevice) {
                                    return;
                                }
    
                                mCaptureSession = cameraCaptureSession;
                                startPreview();
                            }
    
                            @Override
                            public void onConfigureFailed(
                                    @NonNull CameraCaptureSession cameraCaptureSession) {
                                showToast("Failed");
                            }
                        }, null
                );
    
    두 개의 표면이 있는 카메라 세션을 만드는 자바 코드에 따르면 하나는 TextureView, 다른 하나는 ImageReader에서 우리는 camera_manager.cpp에서 해당하는 본체 코드를 작성할 수 있다. 아래와 같다.
    void NDKCamera::CreateSession(ANativeWindow* textureViewWindow, ANativeWindow* imgReaderWindow) {
        auto& req = requests_[PREVIEW_REQUEST_IDX];
        req.outputNativeWindow_ = textureViewWindow;
        req.yuvWindow = imgReaderWindow;
        req.template_ = TEMPLATE_PREVIEW;
    
        ACaptureSessionOutputContainer_create(&outputContainer_);
        CALL_DEV(createCaptureRequest(cameras_[activeCameraId_].device_,
                                              req.template_, &req.request_));
    
        // Add the texture view surface to the container
        ANativeWindow_acquire(req.outputNativeWindow_);
        CALL_OUTPUT(create(req.outputNativeWindow_, &req.sessionOutput_));
        CALL_CONTAINER(add(outputContainer_, req.sessionOutput_));
        CALL_TARGET(create(req.outputNativeWindow_, &req.target_));
        CALL_REQUEST(addTarget(req.request_, req.target_));
    
        // Add the image reader surface to the container
        ANativeWindow_acquire(req.yuvWindow);
        CALL_OUTPUT(create(req.yuvWindow, &req.yuvOutput));
        CALL_CONTAINER(add(outputContainer_, req.yuvOutput));
        CALL_TARGET(create(req.yuvWindow, &req.yuvTarget));
        CALL_REQUEST(addTarget(req.request_, req.yuvTarget));
    
        captureSessionState_ = CaptureSessionState::READY;
        ACameraDevice_createCaptureSession(cameras_[activeCameraId_].device_,
                                      outputContainer_, GetSessionListener(),
                                      &captureSession_);
    }
    
    그런 다음 ACameraCaptureSession_setRepeatingRequest()를 사용하여 미리 보기를 시작할 수 있습니다.
    void NDKCamera::StartPreview(bool start) {
      if (start) {
        ACaptureRequest* requests[] = { requests_[PREVIEW_REQUEST_IDX].request_};
        ACameraCaptureSession_setRepeatingRequest(captureSession_, nullptr, 1,
                                         requests,
                                         nullptr);
      } else if (!start && captureSessionState_ == CaptureSessionState::ACTIVE) {
        ACameraCaptureSession_stopRepeating(captureSession_);
      }
    }
    
    나는 그것이 잘 작동할 수 있을 것이라고 생각했지만, 사실은 그렇지 않았다. 코드를 실행할 때, 화면은 카메라 프레임을 받은 후 동결되었다.다음은 로그입니다.
    2021-12-14 08:42:20.316 24536-24556/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 13, request ID 0, subseq ID 0
    2021-12-14 08:42:21.319 24536-24556/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 14, request ID 0, subseq ID 0
    2021-12-14 08:42:22.321 24536-24584/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 15, request ID 0, subseq ID 0
    2021-12-14 08:42:23.323 24536-24584/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 16, request ID 0, subseq ID 0
    2021-12-14 08:42:24.325 24536-24556/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 17, request ID 0, subseq ID 0
    2021-12-14 08:42:25.328 24536-24584/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 18, request ID 0, subseq ID 0
    2021-12-14 08:42:26.330 24536-24584/com.sample.textureview D/ACameraDevice: Device error received, code 3, frame number 19, request ID 0, subseq ID 0
    
    캡처 세션이 여러 요청을 지원하기 때문에 두 개의 곡면을 각각 두 개의 요청에 넣을 수 있습니까?FPS의 급격한 감소를 야기하는 실험을 수행했습니다.
    yuvReader_ = new ImageReader(&compatibleCameraRes_, AIMAGE_FORMAT_YUV_420_888);
    camera_->CreateSession(ANativeWindow_fromSurface(env_, surface), yuvReader_->GetNativeWindow());
    
    void NDKCamera::CreateSession(ANativeWindow* previewWindow, ANativeWindow* yuvWindow) {
        // Create output from this app's ANativeWindow, and add into output container
        requests_[PREVIEW_REQUEST_IDX].outputNativeWindow_ = previewWindow;
        requests_[PREVIEW_REQUEST_IDX].template_ = TEMPLATE_PREVIEW;
        requests_[YUV_REQUEST_IDX].outputNativeWindow_ = yuvWindow;
        requests_[YUV_REQUEST_IDX].template_ = TEMPLATE_PREVIEW;
    
        CALL_CONTAINER(create(&outputContainer_));
        for (auto& req : requests_) {
            if (!req.outputNativeWindow_) continue;
    
            ANativeWindow_acquire(req.outputNativeWindow_);
            CALL_OUTPUT(create(req.outputNativeWindow_, &req.sessionOutput_));
            CALL_CONTAINER(add(outputContainer_, req.sessionOutput_));
            CALL_TARGET(create(req.outputNativeWindow_, &req.target_));
            CALL_DEV(createCaptureRequest(cameras_[activeCameraId_].device_,
                                          req.template_, &req.request_));
            CALL_REQUEST(addTarget(req.request_, req.target_));
        }
    
        // Create a capture session for the given preview request
        captureSessionState_ = CaptureSessionState::READY;
        CALL_DEV(createCaptureSession(cameras_[activeCameraId_].device_,
                                      outputContainer_, GetSessionListener(),
                                      &captureSession_));
    }
    
    void NDKCamera::StartPreview(bool start) {
      if (start) {
        ACaptureRequest* requests[] = { requests_[PREVIEW_REQUEST_IDX].request_, requests_[YUV_REQUEST_IDX].request_};
        CALL_SESSION(setRepeatingRequest(captureSession_, nullptr, 2,
                                         requests,
                                         nullptr));
      } else if (!start && captureSessionState_ == CaptureSessionState::ACTIVE) {
        ACameraCaptureSession_stopRepeating(captureSession_);
      }
    }
    
    나는 StackOverflow에 이 문제를 만들었다.
    절충된 해결 방안은 두 가지 요청을 만드는 것이다. 하나는 미리 보기 (TextureView 이고 다른 하나는 정지 포획 ImageReader 이다.
    void NDKCamera::CreateSession(ANativeWindow* previewWindow,
                                  ANativeWindow* jpgWindow, bool manualPreview,
                                  int32_t imageRotation) {
      requests_[PREVIEW_REQUEST_IDX].outputNativeWindow_ = previewWindow;
      requests_[PREVIEW_REQUEST_IDX].template_ = TEMPLATE_PREVIEW;
      requests_[JPG_CAPTURE_REQUEST_IDX].outputNativeWindow_ = jpgWindow;
      requests_[JPG_CAPTURE_REQUEST_IDX].template_ = TEMPLATE_STILL_CAPTURE;
    
      CALL_CONTAINER(create(&outputContainer_));
      for (auto& req : requests_) {
        if (!req.outputNativeWindow_) continue;
    
        ANativeWindow_acquire(req.outputNativeWindow_);
        CALL_OUTPUT(create(req.outputNativeWindow_, &req.sessionOutput_));
        CALL_CONTAINER(add(outputContainer_, req.sessionOutput_));
        CALL_TARGET(create(req.outputNativeWindow_, &req.target_));
        CALL_DEV(createCaptureRequest(cameras_[activeCameraId_].device_,
                                      req.template_, &req.request_));
        CALL_REQUEST(addTarget(req.request_, req.target_));
      }
    
      // Create a capture session for the given preview request
      captureSessionState_ = CaptureSessionState::READY;
      CALL_DEV(createCaptureSession(cameras_[activeCameraId_].device_,
                                    outputContainer_, GetSessionListener(),
                                    &captureSession_));
    
      if (jpgWindow) {
        ACaptureRequest_setEntry_i32(requests_[JPG_CAPTURE_REQUEST_IDX].request_,
                                     ACAMERA_JPEG_ORIENTATION, 1, &imageRotation);
      }
    }
    
    Java에서 이미지 캡처를 트리거하는 버튼을 만들었습니다. 나중에 QR코드를 디코딩하는 데 사용합니다.
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:layout_centerHorizontal="true"
                 android:layout_centerVertical="true">
        <TextureView
            android:id="@+id/texturePreview"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="10pt"
            android:textColor="@android:color/white"/>
        <ImageButton
            android:id="@+id/takePhoto"
            android:layout_width="80dp"
            android:layout_height="60dp"
            android:layout_gravity="bottom|center"
            android:src = "@drawable/camera_button"
            android:background="@android:color/transparent"
            android:adjustViewBounds ="true"
            android:scaleType="fitCenter"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="60dp"/>
    
    </FrameLayout>
    
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        onWindowFocusChanged(true);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.textView);
        _takePhoto = (ImageButton)findViewById(R.id.takePhoto);
        _takePhoto.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            scanPhoto(ndkCamera_);
                        }
                    });
        if (isCamera2Device()) {
            RequestCamera();
        } else {
            Log.e("CameraSample", "Found legacy camera device, this sample needs camera2 device");
        }
    }
    
    이미지가 캡처되면 image_reader.cpp에서 다음 콜백 함수가 호출됩니다.
    void ImageReader::ImageCallback(AImageReader *reader) {
      int32_t format;
      media_status_t status = AImageReader_getFormat(reader, &format);
      ASSERT(status == AMEDIA_OK, "Failed to get the media format");
      if (format == AIMAGE_FORMAT_JPEG) {
        AImage *image = nullptr;
        media_status_t status = AImageReader_acquireNextImage(reader, &image);
        ASSERT(status == AMEDIA_OK && image, "Image is not available");
    
      }
      else if (format == AIMAGE_FORMAT_YUV_420_888) {
    
      }
    }
    

    Android 프로젝트에서 타사 공유 라이브러리 링크


    우리는 DynamsoftBarcodeReader.h, DynamsoftCommon.hlibDynamsoftBarcodeReaderAndroid.so를 JNI 폴더로 복사합니다.
    CMakeLists.txt에서 다음 행 링크 공유 라이브러리를 추가합니다.
    target_include_directories(camera_textureview PRIVATE ${COMMON_SOURCE_DIR} ./dbr)
    
    add_library(DynamsoftBarcodeReaderAndroid
            SHARED
            IMPORTED)
    set_target_properties( # Specifies the target library.
            DynamsoftBarcodeReaderAndroid
    
            # Specifies the parameter you want to define.
            PROPERTIES IMPORTED_LOCATION
    
            # Provides the path to the library you want to import.
            ${CMAKE_CURRENT_SOURCE_DIR}/dbr/libDynamsoftBarcodeReaderAndroid.so)
    
    target_link_libraries(camera_textureview dl android log m camera2ndk mediandk DynamsoftBarcodeReaderAndroid)
    
    참고: set_target_properties에서 상대 경로를 사용하지 마십시오.

    2차원 스캐너를 만들다


    우리는 DecodeImage()에서 image_reader.cpp 함수를 만들었다.
    void ImageReader::DecodeImage(AImage* image) {
    
      int planeCount;
      media_status_t status = AImage_getNumberOfPlanes(image, &planeCount);
      ASSERT(status == AMEDIA_OK && planeCount == 1,
             "Error: getNumberOfPlanes() planeCount = %d", planeCount);
      uint8_t *data = nullptr;
      int len = 0;
      AImage_getPlaneData(image, 0, &data, &len);
    
      DBR_DecodeFileInMemory(barcode_reader, data, len, "");
      TextResultArray *handler = NULL;
      DBR_GetAllTextResults(barcode_reader, &handler);
      TextResult **results = handler->results;
      int count = handler->resultsCount;
      std::string out = "No QR Detected";
      if (count > 0)
      {
        out = "";
        for (int index = 0; index < count; index++)
        {
          out += "Index: " + std::to_string(index) + "\n";;
          out += "Barcode format: " + std::string(results[index]->barcodeFormatString) + "\n";
          out += "Barcode value: " + std::string(results[index]->barcodeText) + "\n";
          out += "\n";
    
        }
        DBR_FreeTextResults(&handler);
      }
    
      if (callback_) {
        LOGI("QR detection %s ", out.c_str());
        callback_(callbackCtx_, out.c_str());
      }
      AImage_delete(image);
    }
    
    void ImageReader::ImageCallback(AImageReader *reader) {
      int32_t format;
      media_status_t status = AImageReader_getFormat(reader, &format);
      ASSERT(status == AMEDIA_OK, "Failed to get the media format");
      if (format == AIMAGE_FORMAT_JPEG) {
        AImage *image = nullptr;
        media_status_t status = AImageReader_acquireNextImage(reader, &image);
        ASSERT(status == AMEDIA_OK && image, "Image is not available");
    
        std::thread decodeQRHandler(&ImageReader::DecodeImage, this, image);
        decodeQRHandler.detach();
      }
    }
    
    또한 QR코드 결과를 표시하기 위해 camera_engine.cpp에 콜백 함수를 등록했습니다.
    void CameraAppEngine::OnQRDetected(const char *result) {
      JNIEnv* env;
    
      jvm->AttachCurrentThread(&env, nullptr);
      jmethodID methodID = env->GetMethodID(globalClass, "onQRDetected", "(Ljava/lang/String;)V");
      jstring javaName = env->NewStringUTF(result);
    
      env->CallVoidMethod(javaInstance_, methodID, javaName);
      jvm->DetachCurrentThread();
    }
    
    void CameraAppEngine::CreateCameraSession(jobject surface) {
      surface_ = env_->NewGlobalRef(surface);
      jpgReader_ = new ImageReader(&compatibleCameraRes_, AIMAGE_FORMAT_JPEG);
      jpgReader_->SetPresentRotation(GetCameraSensorOrientation(ACAMERA_LENS_FACING_BACK));
      jpgReader_->RegisterCallback(this, [this](void* ctx, const char* str) -> void {
          reinterpret_cast<CameraAppEngine* >(ctx)->OnQRDetected(str);
      });
     camera_->CreateSession(ANativeWindow_fromSurface(env_, surface), jpgReader_->GetNativeWindow(), false, GetCameraSensorOrientation(ACAMERA_LENS_FACING_BACK));
    }
    
    이제 우리는 사진을 찍어서 QR코드를 디코딩할 수 있다.만약 우리가 QR코드를 실시간으로 디코딩하고 싶다면?정답:
    Timer timer = new Timer();
    
    public void scheduleTask() {
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (ndkCamera_ != 0 && surface_ != null) {
                    scanPhoto(ndkCamera_);
                }
            }
        }, 0);
    }
    
    public void onQRDetected(String result) {
        final String content = result;
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                textView.setText(content);
                scheduleTask();
            }
        });
    }
    

    자바에서 C++로 Camera2 코드를 마이그레이션하는 것이 좋습니까?


    전체 과정은 제가camera2 API가 엔진 뚜껑 아래에서 어떻게 작동하는지 이해하는 데 도움을 주었지만 제 답은No입니다.NDK Camera2 API를 사용한 경험이 기대에 미치지 못했습니다.그것은 심지어 자바API를 사용하는 것보다 더 나쁘다.실제로 자바에서 ImageReader 대상을 만들 때 관련 메모리는 본 컴퓨터 더미에 분배됩니다.따라서 이미지 버퍼를 가져오고 본 컴퓨터 코드로 이미지 처리 API를 호출하면 추가 메모리 복사와 GC가 없습니다.

    관련 기사


    만약 당신이 안드로이드 C++ 프로그래밍에 관심이 있다면, 당신도 이 글을 읽을 수 있습니다. How to Build a QR Code Scanner for Windows and Android with Qt QML

    도구책

  • https://android.googlesource.com/platform/frameworks/av/+/c360382/media/ndk/
  • https://android.googlesource.com/platform/cts/+/master/tests/camera/libctscamera2jni/native-camera-jni.cpp
  • 소스 코드


    https://github.com/yushulx/ndk-native-camera2-

    좋은 웹페이지 즐겨찾기