UVCAndroid, a common development library for Android UVC cameras (supports multiple previews and multiple cameras)

Introduction

UVCAndroid is a general-purpose development library for Android UVC cameras.

GitHub source address : https://github.com/shiyinghan/UVCAndroid

The main function

The main functions include:
(1) Support USB Camera device detection, real-time preview of the screen;
(2) Support snapping pictures in jpg format, and can set the picture compression quality ;
(3) Support recording mp4 format video, can block audio, and can set video and audio (
4) Support obtaining the resolution supported by the camera, and resolution switching; (5 )
Support the preview to automatically identify the resolution of various cameras; (
6) Support rotating the camera 90 degrees, 180 degrees, 270 degrees ;
( 7) Support adjustment of some camera control parameters such as contrast, brightness, hue, saturation, white balance, etc.;
(8) Support multiple previews and multiple cameras;
(9) Support Android5.0+;

how to use

1. Add dependencies to the local project

The first step is to add the mavenCentral repository to the project gradle file
Step 1. Add the mavenCentral repository to your build file
Add it in your root build.gradle at the end of repositories:

allprojects {
    
    
    repositories {
    
    
		...
		mavenCentral()
    }
}

The second step is to add dependencies to the gradle file of the app Module

dependencies {
    
    
    implementation 'com.herohan:UVCAndroid:1.0.4'
}

2. Get permission

Request permissions

 	List<String> needPermissions = new ArrayList<>();
    needPermissions.add(Manifest.permission.CAMERA);
    needPermissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);//拍照和录制视频时需要该权限
    //needPermissions.add(Manifest.permission.MANAGE_EXTERNAL_STORAGE); //Android 11 使用该权限替代 WRITE_EXTERNAL_STORAGE
    needPermissions.add(Manifest.permission.RECORD_AUDIO);//录制视频时需要音频时需要该权限

	//这里使用XXPermissions开源框架获取权限,你也可以使用系统原生的,或者其他开源框架获取权限
    XXPermissions.with(this)
            .permission(needPermissions)
            .request((permissions, all) -> {
    
    
                if(!all){
    
    
                    return;
                }
	
	//摄像头业务操作
           });
<application
	...
	android:requestLegacyExternalStorage="true"
	>

3. Initialize the UVC business class, set the UVC camera status callback, and set the Surface monitoring callback of TextureView or SurfaceView

Initialize CameraHelper,set UVC Camera state callback

 private ICameraHelper mCameraHelper;
 private AspectRatioSurfaceView mCameraViewMain;
 private ICameraHelper.StateCallback mStateListener;

	//UVC摄像头状态回调
	mStateListener = new ICameraHelper.StateCallback() {
    
    
		//插入UVC设备
        @Override
        public void onAttach(UsbDevice device) {
    
    
        	//设置为当前设备(如果没有权限,会显示授权对话框)
            mCameraHelper.selectDevice(device);
        }

		//打开UVC设备成功(也就是已经获取到UVC设备的权限)
        @Override
        public void onDeviceOpen(UsbDevice device, boolean isFirstOpen) {
    
    
        	//打开UVC摄像头
            mCameraHelper.openCamera();
        }

		//打开摄像头成功
        @Override
        public void onCameraOpen(UsbDevice device) {
    
    
        	//开始预览
            mCameraHelper.startPreview();

			//获取预览使用的Size(包括帧格式、宽度、高度、FPS)
            Size size = mCameraHelper.getPreviewSize();
            if (size != null) {
    
    
                int width = size.width;
                int height = size.height;
                //需要自适应摄像头分辨率的话,设置新的宽高比
                mCameraViewMain.setAspectRatio(width, height);
            }

			//添加预览Surface
            mCameraHelper.addSurface(mCameraViewMain.getHolder().getSurface(), false);
        }

		//关闭摄像头成功
        @Override
        public void onCameraClose(UsbDevice device) {
    
    
            if (mCameraHelper != null) {
    
    
            	//移除预览Surface
                mCameraHelper.removeSurface(mCameraViewMain.getHolder().getSurface());
            }
        }

		//关闭UVC设备成功
        @Override
        public void onDeviceClose(UsbDevice device) {
    
    
        }

		//断开UVC设备
        @Override
        public void onDetach(UsbDevice device) {
    
    
        }

		//用户没有授予访问UVC设备的权限
        @Override
        public void onCancel(UsbDevice device) {
    
    
        }

    };
    
    //设置SurfaceView的Surface监听回调
    mCameraViewMain.getHolder().addCallback(new SurfaceHolder.Callback() {
    
    
    
    		//创建了新的Surface
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder holder) {
    
    
                if (mCameraHelper != null) {
    
    
                	//添加预览Surface
                    mCameraHelper.addSurface(holder.getSurface(), false);
                }
            }
			
			//Surface发生了改变
            @Override
            public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
    
    
            }

			//销毁了原来的Surface
            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
    
    
                if (mCameraHelper != null) {
    
    
                	//移除预览Surface
                    mCameraHelper.removeSurface(holder.getSurface());
                }
            }
        });
        
		mCameraHelper = new CameraHelper();
		//设置UVC摄像头状态回调
        mCameraHelper.setStateCallback(mStateListener);

4. Release the UVC business class (including canceling the UVC camera status callback, stopping Camera preview, closing Camera, etc.)

Release CameraHelper(including canceling UVC Camera state callback, stopping Camera preview, etc.)

 mCameraHelper.release();

5. Picture capture

Image Capture

	//设置视图片抓拍全局参数(非必须,可以不设置,使用默认值)
	mCameraHelper.setImageCaptureConfig(
                mCameraHelper.getImageCaptureConfig().setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY));
//        mCameraHelper.setImageCaptureConfig(
//                mCameraHelper.getImageCaptureConfig().setJpegCompressionQuality(90));

	//设置需要保存图片文件
 	File file = FileUtils.getCaptureFile(this, Environment.DIRECTORY_DCIM, ".jpg");
    ImageCapture.OutputFileOptions options =
             new ImageCapture.OutputFileOptions.Builder(file).build();

//                ContentValues contentValues = new ContentValues();
//                contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_IMAGE");
//                contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
//
//                ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(
//                        getContentResolver(),
//                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
//                        contentValues).build();

//                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//                ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(outputStream).build();

	//进行图片抓拍
    mCameraHelper.takePicture(options, new ImageCapture.OnImageCaptureCallback() {
    
    
    	//图片抓拍成功
       	@Override
        public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
    
    
            Toast.makeText(TakePictureActivity.this,
                    "save \"" + UriHelper.getPath(TakePictureActivity.this, outputFileResults.getSavedUri()) + "\"",
                    Toast.LENGTH_SHORT).show();
        }

		//图片抓拍出现错误
        @Override
        public void onError(int imageCaptureError, @NonNull String message, @Nullable Throwable cause) {
    
    
            Toast.makeText(TakePictureActivity.this, message, Toast.LENGTH_SHORT).show();
        }
    });

6. Record video

Video Capture

	//设置视频录制全局参数(非必须,可以不设置,使用默认值)
	mCameraHelper.setVideoCaptureConfig(
	    mCameraHelper.getVideoCaptureConfig()
	              .setAudioCaptureEnable(true) // true:有音频;false:没有音频(默认为true)
	              .setBitRate((int) (1024 * 1024 * 25 * 0.25))
	              .setVideoFrameRate(25)
	              .setIFrameInterval(1));

	//设置需要保存视频文件
	File file = FileUtils.getCaptureFile(this, Environment.DIRECTORY_MOVIES, ".mp4");
    VideoCapture.OutputFileOptions options =
            new VideoCapture.OutputFileOptions.Builder(file).build();

//        ContentValues contentValues = new ContentValues();
//        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
//        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
//
//        VideoCapture.OutputFileOptions options = new VideoCapture.OutputFileOptions.Builder(
//                getContentResolver(),
//                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
//                contentValues).build();

	//开始录制
    mCameraHelper.startRecording(options, new VideoCapture.OnVideoCaptureCallback() {
    
    
        @Override
        public void onStart() {
    
    
        }

		//视频录制成功
        @Override
        public void onVideoSaved(@NonNull VideoCapture.OutputFileResults outputFileResults) {
    
    
            Toast.makeText(
                    RecordVideoActivity.this,
                    "save \"" + UriHelper.getPath(RecordVideoActivity.this, outputFileResults.getSavedUri()) + "\"",
                    Toast.LENGTH_SHORT).show();
        }
		
		//视频录制出现错误
        @Override
        public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) {
    
    
            Toast.makeText(RecordVideoActivity.this, message, Toast.LENGTH_LONG).show();
        }
    });

7. Change camera preview parameters (including frame format, width, height, FPS)

Set camera preview parameters (including frame format, width, height, FPS)

	//停止相机预览
	mCameraHelper.stopPreview();
	//设置摄像机预览参数
	mCameraHelper.setPreviewSize(size);
	//开始相机预览
	mCameraHelper.startPreview();

	//需要自适应摄像头分辨率的话,设置新的宽高比
	mCameraViewMain.setAspectRatio(mPreviewWidth, mPreviewHeight);

8. Adjust some camera control parameters such as contrast, brightness, hue, saturation, white balance, etc.

Adjust contrast, brightness, hue, saturation, white balance, and other camera controls

//获取UVCControl对象,通过该对象调整相机控制参数
UVCControl control = mCameraHelper.getUVCControl();

//根据监听器设置各种相机控制参数
private void setAllControlChangeListener(UVCControl controls) {
    
    
        // Brightness
        isbBrightness.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setBrightness(seekParams.progress));
        // Contrast
        isbContrast.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setContrast(seekParams.progress));
        // Contrast Auto
        cbContrastAuto.setOnCheckedChangeListener((buttonView, isChecked) -> {
    
    
            controls.setContrastAuto(isChecked);
        });
        // Hue
        isbHue.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setHue(seekParams.progress));
        // Hue Auto
        cbHueAuto.setOnCheckedChangeListener((buttonView, isChecked) -> {
    
    
            controls.setHueAuto(isChecked);
        });
        // Saturation
        isbSaturation.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setSaturation(seekParams.progress));
        // Sharpness
        isbSharpness.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setSharpness(seekParams.progress));
        // Gamma
        isbGamma.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setGamma(seekParams.progress));
        // White Balance
        isbWhiteBalance.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setWhiteBalance(seekParams.progress));
        // White Balance Auto
        cbWhiteBalanceAuto.setOnCheckedChangeListener((buttonView, isChecked) -> {
    
    
            controls.setWhiteBalanceAuto(isChecked);
        });
        // Backlight Compensation
        isbBacklightComp.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setBacklightComp(seekParams.progress));
        // Gain
        isbGain.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setGain(seekParams.progress));
        // Exposure Time
        isbExposureTime.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setExposureTimeAbsolute(seekParams.progress));
        // Exposure Time Auto
        cbExposureTimeAuto.setOnCheckedChangeListener((buttonView, isChecked) -> {
    
    
            controls.setExposureTimeAuto(isChecked);
        });
        // Iris
        isbIris.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setIrisAbsolute(seekParams.progress));
        // Focus
        isbFocus.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setFocusAbsolute(seekParams.progress));
        // Focus Auto
        cbFocusAuto.setOnCheckedChangeListener((buttonView, isChecked) -> {
    
    
            controls.setFocusAuto(isChecked);
        });
        // Zoom
        isbZoom.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setZoomAbsolute(seekParams.progress));
        // Pan
        isbPan.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setPanAbsolute(seekParams.progress));
        // Tilt
        isbTilt.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setTiltAbsolute(seekParams.progress));
        // Roll
        isbRoll.setOnSeekChangeListener(
                (MyOnSeekChangeListener) seekParams -> controls.setRollAbsolute(seekParams.progress));
        // Power Line Frequency
        rgPowerLineFrequency.setOnCheckedChangeListener((group, checkedId) -> {
    
    
            int value = 0;
            if (checkedId == R.id.rbPowerLineFrequencyDisable) {
    
    
                value = 0;
            } else if (checkedId == R.id.rbPowerLineFrequency50Hz) {
    
    
                value = 1;
            } else if (checkedId == R.id.rbPowerLineFrequency60Hz) {
    
    
                value = 2;
            } else if (checkedId == R.id.rbPowerLineFrequencyAuto) {
    
    
                value = 3;
            }
            controls.setPowerlineFrequency(value);
        });
    }

// 重置所有相机控制参数为初试值
private void resetAllControlParams(UVCControl control) {
    
    
        // Brightness
        control.resetBrightness();
        // Contrast
        control.resetContrast();
        // Contrast Auto
        control.resetContrastAuto();
        // Hue
        control.resetHue();
        // Hue Auto
        control.resetHueAuto();
        // Saturation
        control.resetSaturation();
        // Sharpness
        control.resetSharpness();
        // Gamma
        control.resetGamma();
        // White Balance
        control.resetWhiteBalance();
        // White Balance Auto
        control.resetWhiteBalanceAuto();
        // Backlight Compensation
        control.resetBacklightComp();
        // Gain
        control.resetGain();
        // Exposure Time
        control.resetExposureTimeAbsolute();
        // Auto-Exposure Mode
        control.resetAutoExposureMode();
        // Iris
        control.resetIrisAbsolute();
        // Focus
        control.resetFocusAbsolute();
        // Focus Auto
        control.resetFocusAuto();
        // Zoom
        control.resetZoomAbsolute();
        // Pan
        control.resetPanAbsolute();
        // Tilt
        control.resetTiltAbsolute();
        // Roll
        control.resetRollAbsolute();
        // Power Line Frequency
        control.resetPowerlineFrequency();
    }

9. Rotate the camera 90 degrees, 180 degrees, 270 degrees, set the camera preview mirror

Rotate the camera 90 degrees, 180 degrees, and 270 degrees , set the camera preview mirror

	//旋转摄像头
	private void rotateBy(int angle) {
    
    
        mPreviewRotation += angle;
        mPreviewRotation %= 360;
        if (mPreviewRotation < 0) {
    
    
            mPreviewRotation += 360;
        }

        if (mCameraHelper != null) {
    
    
            mCameraHelper.setPreviewConfig(
                    mCameraHelper.getPreviewConfig().setRotation(mPreviewRotation));
        }
    }

	//设置水平镜像显示
    private void flipHorizontally() {
    
    
        if (mCameraHelper != null) {
    
    
            mCameraHelper.setPreviewConfig(
                    mCameraHelper.getPreviewConfig().setMirror(MirrorMode.MIRROR_HORIZONTAL));
        }
    }

	//设置垂直镜像显示
    private void flipVertically() {
    
    
        if (mCameraHelper != null) {
    
    
            mCameraHelper.setPreviewConfig(
                    mCameraHelper.getPreviewConfig().setMirror(MirrorMode.MIRROR_VERTICAL));
        }
    }

10. Set up multiple previews

Set multiple previews

mCameraHelper.addSurface(svCameraViewMain.getHolder().getSurface(), false);
mCameraHelper.addSurface(svCameraViewSecond.getHolder().getSurface(), false);
mCameraHelper.addSurface(svCameraViewThird.getHolder().getSurface(), false);
mCameraHelper.addSurface(svCameraViewFourth.getHolder().getSurface(), false);

11. Set multiple cameras (USB2.0 is limited by bandwidth, it may not be possible to connect multiple cameras at the same time)

Setting multiple Cameras

	private ICameraHelper mCameraHelperLeft;
    private ICameraHelper mCameraHelperRight;

    private AspectRatioSurfaceView svCameraViewLeft;
    private AspectRatioSurfaceView svCameraViewRight;

	private UsbDevice mUsbDeviceLeft;
    private UsbDevice mUsbDeviceRight;

	private final ICameraHelper.StateCallback mStateListenerLeft = new ICameraHelper.StateCallback() {
    
    
		@Override
        public void onAttach(UsbDevice device) {
    
    
            synchronized (mSync) {
    
    
                if (mUsbDeviceLeft == null && !device.equals(mUsbDeviceRight)) {
    
    
                    selectDeviceLeft(device);
                }
            }
        }
        @Override
        public void onDeviceOpen(UsbDevice device, boolean isFirstOpen) {
    
    
            if (device.equals(mUsbDeviceLeft)) {
    
    
                UVCParam param = new UVCParam();
                param.setQuirks(UVCCamera.UVC_QUIRK_FIX_BANDWIDTH);
                mCameraHelperLeft.openCamera(param);
            }
        }
        @Override
        public void onCameraOpen(UsbDevice device) {
    
    
            if (device.equals(mUsbDeviceLeft)) {
    
    
                mCameraHelperLeft.startPreview();

                Size size = mCameraHelperLeft.getPreviewSize();
                if (size != null) {
    
    
                    int width = size.width;
                    int height = size.height;
                    //auto aspect ratio
                    svCameraViewLeft.setAspectRatio(width, height);
                }

                mCameraHelperLeft.addSurface(svCameraViewLeft.getHolder().getSurface(), false);

                mIsCameraLeftConnected = true;
            }
        }

        @Override
        public void onCameraClose(UsbDevice device) {
    
    
            if (device.equals(mUsbDeviceLeft)) {
    
    
                if (mCameraHelperLeft != null) {
    
    
                   mCameraHelperLeft.removeSurface(svCameraViewLeft.getHolder().getSurface());
                }

                mIsCameraLeftConnected = false;
            }
        }

        @Override
        public void onDeviceClose(UsbDevice device) {
    
    
        }

        @Override
        public void onDetach(UsbDevice device) {
    
    
            if (device.equals(mUsbDeviceLeft)) {
    
    
                mUsbDeviceLeft = null;
            }
        }

        @Override
        public void onCancel(UsbDevice device) {
    
    
            if (device.equals(mUsbDeviceLeft)) {
    
    
                mUsbDeviceLeft = null;
            }
        }
	};
	private final ICameraHelper.StateCallback mStateListenerRight = new ICameraHelper.StateCallback() {
    
    
		@Override
        public void onAttach(UsbDevice device) {
    
    
            synchronized (mSync) {
    
    
                if (mUsbDeviceRight == null && !device.equals(mUsbDeviceLeft)) {
    
    
                    selectDeviceRight(device);
                }
            }
        }

        @Override
        public void onDeviceOpen(UsbDevice device, boolean isFirstOpen) {
    
    
            if (device.equals(mUsbDeviceRight)) {
    
    
                UVCParam param = new UVCParam();
                param.setQuirks(UVCCamera.UVC_QUIRK_FIX_BANDWIDTH);
                mCameraHelperRight.openCamera(param);
            }
        }

        @Override
        public void onCameraOpen(UsbDevice device) {
    
    
            if (device.equals(mUsbDeviceRight)) {
    
    
                mCameraHelperRight.startPreview();

                Size size = mCameraHelperRight.getPreviewSize();
                if (size != null) {
    
    
                    int width = size.width;
                    int height = size.height;
                    //auto aspect ratio
                    svCameraViewRight.setAspectRatio(width, height);
                }

                mCameraHelperRight.addSurface(svCameraViewRight.getHolder().getSurface(), false);

                mIsCameraRightConnected = true;
            }
        }

        @Override
        public void onCameraClose(UsbDevice device) {
    
    
            if (device.equals(mUsbDeviceRight)) {
    
    
                if (mCameraHelperRight != null) {
    
    
                    mCameraHelperRight.removeSurface(svCameraViewRight.getHolder().getSurface());
                }

                mIsCameraRightConnected = false;
            }
        }

        @Override
        public void onDeviceClose(UsbDevice device) {
    
    
        }

        @Override
        public void onDetach(UsbDevice device) {
    
    
            if (device.equals(mUsbDeviceRight)) {
    
    
                mUsbDeviceRight = null;
            }
        }

        @Override
        public void onCancel(UsbDevice device) {
    
    
            if (device.equals(mUsbDeviceRight)) {
    
    
                mUsbDeviceRight = null;
            }
        }
	};

	svCameraViewLeft.getHolder().addCallback(new SurfaceHolder.Callback() {
    
    
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder holder) {
    
    
                if (mCameraHelperLeft != null) {
    
    
                    mCameraHelperLeft.addSurface(holder.getSurface(), false);
                }
            }

            @Override
            public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
    
    

            }

            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
    
    
                if (mCameraHelperLeft != null) {
    
    
                    mCameraHelperLeft.removeSurface(holder.getSurface());
                }
            }
        });
	svCameraViewRight.setAspectRatio(DEFAULT_WIDTH, DEFAULT_HEIGHT);

        svCameraViewRight.getHolder().addCallback(new SurfaceHolder.Callback() {
    
    
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder holder) {
    
    
                if (mCameraHelperRight != null) {
    
    
                    mCameraHelperRight.addSurface(holder.getSurface(), false);
                }
            }

            @Override
            public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
    
    

            }

            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
    
    
                if (mCameraHelperRight != null) {
    
    
                    mCameraHelperRight.removeSurface(holder.getSurface());
                }
            }
        });

	mCameraHelperLeft = new CameraHelper();
	mCameraHelperLeft.setStateCallback(mStateListenerLeft);
	mCameraHelperRight = new CameraHelper();
	mCameraHelperRight.setStateCallback(mStateListenerRight);

Other APIs

method illustrate
getDeviceList() Get all UVC devices currently detected
getSupportedFormatList() Get the Format list supported by the current camera
getSupportedSizeList() Get the Size list supported by the current camera
getPreviewSize() Get the preview size currently used by the camera
setButtonCallback() Set button event callback
setFrameCallback() Set the real-time preview image data callback ( please call it in the onDeviceOpen or onCameraOpen callback function of StateCallback, the method can refer to SetFrameCallbackActivity in the demo ), support the format UVCCamera.PIXEL_FORMAT_YUV; PIXEL_FORMAT_NV12; PIXEL_FORMAT_NV21; PIXEL_FORMAT_RGB565; PIXEL_FORMAT_RGBX and other formats
openCamera(Size size) Open the current camera with the specified format
closeCamera() close current camera
isRecording() Is it recording
isCameraOpened() Whether the current camera has been opened

Download Demo APK

Download demo APK
insert image description here
app-release.apk

insert image description here
demo-release.apk

reference

saki4510t/UVCCamera

Guess you like

Origin blog.csdn.net/hanshiying007/article/details/124118486