UVCAndroid,安卓UVC相机通用开发库(支持多预览和多摄像头)

1 篇文章 1 订阅

简介

UVCAndroid是一款用于安卓UVC相机的通用开发库。

GitHub源码地址https://github.com/shiyinghan/UVCAndroid

主要功能

主要功能包括:
(1) 支持USB Camera设备检测,画面实时预览;
(2) 支持抓拍jpg格式图片,可设置图片压缩质量;
(3) 支持录制mp4格式视频,可屏蔽音频,可设置视频和音频的录制参数;
(4) 支持获取camera支持的分辨率,和分辨率切换;
(5) 支持预览自动识别各种相机的分辨率;
(6) 支持旋转摄像头90度、180度、270度
(7) 支持调整对比度、亮度、色调、饱和度、白平衡等等一些相机控制参数;
(8) 支持多预览和多摄像头;
(9) 支持Android5.0+;

如何使用

1. 添加依赖到本地工程

第一步 添加mavenCentral仓库到工程gradle文件
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()
    }
}

第二步 添加依赖到app Module的gradle文件

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

2. 获取权限

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. 初始化UVC业务类,设置UVC摄像头状态回调,设置TextureView或者SurfaceView的Surface监听回调

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. 释放UVC业务类(包含取消UVC摄像头状态回调,停止Camera预览,关闭Camera等操作)

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

 mCameraHelper.release();

5. 图片抓拍

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. 录制视频

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. 改变摄像机预览参数(包括帧格式、宽度、高度、FPS)

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

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

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

Size里面的帧格式type只支持UVCCamera.UVC_VS_FRAME_UNCOMPRESSED(YUV格式)和UVCCamera.UVC_VS_FRAME_MJPEG(Mjpeg格式),使用的时候需要摄像头支持(摄像头可能支持YUV格式或者Mjpeg格式)。

8. 调整对比度、亮度、色调、饱和度、白平衡等等一些相机控制参数

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.旋转摄像头90度、180度、270度,设置摄像头预览镜像

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 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.设置多个摄像头(USB2.0受带宽所限,有可能无法同时连接多个摄像头)

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);

其他API

方法说明
getDeviceList()获取当前检测到的所有UVC设备
getSupportedFormatList()获取当前摄像头支持的Format列表
getSupportedSizeList()获取当前摄像头支持的Size列表
getPreviewSize()获取当前摄像头正在使用的预览Size
setButtonCallback()设置按钮事件回调
setFrameCallback()设置实时预览图像数据回调(请在StateCallback的onDeviceOpen或者onCameraOpen回调函数里面调用,使用方法可以参考demo里面的SetFrameCallbackActivity),支持格式 UVCCamera.PIXEL_FORMAT_YUV;PIXEL_FORMAT_NV12;PIXEL_FORMAT_NV21;PIXEL_FORMAT_RGB565;PIXEL_FORMAT_RGBX等格式
openCamera(Size size)用指定格式打开当前摄像头
closeCamera()关闭当前摄像头
isRecording()是否正在录像
isCameraOpened()是否已经打开当前摄像头

下载演示APK

Download demo APK
在这里插入图片描述
app-debug.apk

在这里插入图片描述
demo-debug.apk

参考

saki4510t/UVCCamera

  • 35
    点赞
  • 102
    收藏
    觉得还不错? 一键收藏
  • 122
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 122
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值