Android APP Camera2应用(03)拍照&保存照片流程

49 篇文章 15 订阅

说明:camera子系统 系列文章针对Android10.0系统,主要针对 camera API2 + HAL3 框架进行解读。

1 拍照&保存照片流程简要解读

在完成预览操作之后,点击拍照按钮,触发拍照操作。由 button点击来触发拍照事件,使用 ImageReader访问呈现到Surface中的图像并保存,所以在预览的 Surface捕获图像的同时, 我们也需要 ImageReader来同时捕获图像数据,所以在上一章节中第二部分@5步中,CameraDevice.CreateCaptureSession()方法中,我们将 ImageReader的实例也传入第一个参数中(实际上是在预览时就做好了准备),即:

Arrays.asList(surface,ImageReader.getSurface())

@1 调用 CameraDevice.CreateCapture(int templateType),创建一个 CaptureRequest.Builder,templateType来区分是拍照还是预览,拍照时我们传入CameraDevice.TEMPLATE_STILL_CAPTURE参数,该方法回返回一个对于返回的对象,我们声明一个 CaptureRequest.Builder类型的mCaptureRequestBuilder变量来接收。关键代码如下所示:

// camera capture process,step1 创建作为拍照的CaptureRequest.Builder 
​​​​​​​mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); 

@2 调用 mCaptureRequestBuilder.addTarget()方法,将访图像信息的 ImageReader 实例传入,即将 ImageReader 的实例,添加到该请求的目标列表中。

// camera capture process,step2 将imageReader的surface作为CaptureRequest.Builder的目标
mCaptureRequestBuilder.addTarget(mImageReader.getSurface());

@3 创建ImagerReader.OnImageAvailableListener 方法的实例,并重写OnImageAvailable 方法,该方法在 ImageReader中的图像信息可用的时候执行,代码如下所示:

//camera capture process,step3 创建ImageReader并设置mImageAvailableListener,实现如下:
private ImageReader.OnImageAvailableListener mImageAvailableListener = new ImageReader.OnImageAvailableListener() {
    @Override
    public void onImageAvailable(ImageReader reader) {
        if(mState == State.STATE_PREVIEW){
            //Log.d(TAG, "##### onFrame: Preview");
            Image image = reader.acquireNextImage();
            image.close();
        }else if(mState == State.STATE_CAPTURE) {
            Log.d(TAG,"capture one picture to gallery");
            mCameraFile = new File("aa_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".jpg");
            mCameraHandler.post(new ImageSaver(mContext, reader.acquireLatestImage(), mCameraFile));
            mState = State.STATE_PREVIEW;
        }else if(mState == State.STATE_RECORD) {
            Log.d(TAG,"record video");

        }else{
            Log.d(TAG, "##### onFrame: default/nothing");
        }
    }
};

实际上无论是预览还是拍照,最终都通过该listener来监听。

@4 处理 OnImageAvailable 回调中拿到的图像信息,即拍照后进行存储,关键代码为ImageSaver的实现,相关代码如下所示:

//camera capture progress,step4 保存图片相关操作
public static class ImageSaver implements Runnable {
    private final Image mImage;
    private final File mFile;
    Context mContext;

    ImageSaver(Context context,Image image, File file) {
        mContext = context;
        mImage = image;
        mFile = file;
    }

    @Override
    public void run() {
        Log.d(TAG,"take picture Image Run");
        ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DESCRIPTION, "This is an qr image");
        values.put(MediaStore.Images.Media.DISPLAY_NAME, mFile.getName());
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        values.put(MediaStore.Images.Media.TITLE, "Image.jpg");
        values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/");
        Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        ContentResolver resolver = mContext.getContentResolver();
        Uri insertUri = resolver.insert(external, values);
        OutputStream os = null;
        try {
            if (insertUri != null) {
                os = resolver.openOutputStream(insertUri);
            }
            if (os != null) {
                os.write(bytes);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            mImage.close();
            try {
                if(os!=null) {
                    os.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

@5 调用 CameraCaputureSession.capture()方法进行拍照,该方法传入三个参数:

  • CaptureRequest:此次拍照的参数设置。
  • CaptureCallback:callback 对象,当这个请求被处理的时候触发,如果为 null,不会生成 matedate 信息,但是仍会生成图像信息。
  • Handler:为一个句柄,代表执行 callback 的 handler,如果程序希望直接在当前线程中执行 callback,则可以将 handler 参数设为 null。

关键代码整合在6中,因为5 6 代码紧密相关。

@6 创建 CameraCaptureSession.CaptureCallback实例,并重写onCaptureCompleted方法,当图图像捕获的流程向前进行的时候,该方法被调用。执行 CameraCaptureSession.CaptureCallback 中的 OnCaptureCompleted 回调,该回调会在图像捕获完成并且所有的结果信息可用的时候执行,恢复预览状态,即重新调用预览方法,相关代码如下所示:

// camera capture process,step5 &6 捕获静态图像,结束后执行onCaptureCompleted
mCaptureSession.capture(mCaptureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() {
    @Override// 拍照完成时激发该方法
    public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
        Log.d(TAG,"onCaptureCompleted");
        //恢复预览
        startCaptureSession();
    }
}, mCameraHandler);

2 camera拍照&保存照片流程代码完整解读

2.1 java源码部分(草稿)

Camera流程相关代码如下所示:

class CameraCoreManager {
    private static final String TAG = "CameraDemo";

    private Context mContext;
    private CameraManager mCameraManager;
    private String mCameraId;
    private HandlerThread mCameraThread;
    private Handler mCameraHandler;
    private ImageReader mImageReader;
    private CameraDevice mCameraDevice;
    private CameraCharacteristics mCameraCharacteristics;
    private MediaRecorder mMediaRecorder;

    //Max preview width&height that is guaranteed by Camera2 API
    private static final int MAX_PREVIEW_WIDTH = 1920;
    private static final int MAX_PREVIEW_HEIGHT = 1080;

    //A Semaphore to prevent the app from exiting before closing the camera.
    private Semaphore mCameraOpenCloseLock = new Semaphore(1);
    private Size mPreviewSize = new Size(1920, 1080);
    private CaptureRequest.Builder mPreviewRequestBuilder;
    private CaptureRequest.Builder mCaptureRequestBuilder;
    private CameraCaptureSession mCaptureSession;
    private int mFacing = CameraCharacteristics.LENS_FACING_BACK;
    private Choreographer.FrameCallback mFrameCallback;
    private SurfaceTexture mSurfaceTexture;
    private File mCameraFile;

    private enum State{
        STATE_PREVIEW,
        STATE_CAPTURE,
        STATE_RECORD
    }
    State mState = State.STATE_PREVIEW;

    //camera capture process,step3 创建ImageReader并设置mImageAvailableListener,实现如下:
    private ImageReader.OnImageAvailableListener mImageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            if(mState == State.STATE_PREVIEW){
                //Log.d(TAG, "##### onFrame: Preview");
                Image image = reader.acquireNextImage();
                image.close();
            }else if(mState == State.STATE_CAPTURE) {
                Log.d(TAG,"capture one picture to gallery");
                mCameraFile = new File("aa_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".jpg");
                mCameraHandler.post(new ImageSaver(mContext, reader.acquireLatestImage(), mCameraFile));
                mState = State.STATE_PREVIEW;
            }else if(mState == State.STATE_RECORD) {
                Log.d(TAG,"record video");

            }else{
                Log.d(TAG, "##### onFrame: default/nothing");
            }
        }
    };

    //camera preview process,step2 mStateCallback 实例化
    private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            //重写onOpened方法,最为关键
            mCameraOpenCloseLock.release();
            mCameraDevice = camera;
            startCaptureSession();
        }

        @Override
        public void onDisconnected(CameraDevice camera) {
            mCameraOpenCloseLock.release();
            camera.close();
            mCameraDevice = null;
        }

        @Override
        public void onError(CameraDevice camera, int error) {
            Log.e("DEBUG", "onError: " + error);
            mCameraOpenCloseLock.release();
            camera.close();
            mCameraDevice = null;
            Log.e("DEBUG", "onError:  restart camera");
            stopPreview();
            startPreview();
        }
    };

    CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureCompleted(CameraCaptureSession session,CaptureRequest request, TotalCaptureResult result) {
            super.onCaptureCompleted(session, request, result);
        }

        @Override
        public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
            super.onCaptureFailed(session, request, failure);
        }
    };

    public CameraCoreManager(Context context) {
        mContext = context;
        mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
        mMediaRecorder = new MediaRecorder();
        mState = State.STATE_PREVIEW;
    }

    public void startPreview() {
        if (!chooseCameraIdByFacing()) {
            Log.e(TAG, "Choose camera failed.");
            return;
        }

        mCameraThread = new HandlerThread("CameraThread");
        mCameraThread.start();
        mCameraHandler = new Handler(mCameraThread.getLooper());

        if (mImageReader == null) {
            mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.JPEG, 2);
            mImageReader.setOnImageAvailableListener(mImageAvailableListener, mCameraHandler);
        }else{
            mImageReader.close();
        }
        openCamera();
    }

    public void stopPreview() {
        closeCamera();
        if (mCameraThread != null) {
            mCameraThread.quitSafely();
            mCameraThread = null;
        }
        mCameraHandler = null;
    }

    private boolean chooseCameraIdByFacing() {
        try {
            String ids[] = mCameraManager.getCameraIdList();
            if (ids.length == 0) {
                Log.e(TAG, "No available camera.");
                return false;
            }

            for (String cameraId : mCameraManager.getCameraIdList()) {
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);

                StreamConfigurationMap map = characteristics.get(
                        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                if (map == null) {
                    continue;
                }

                Integer level = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
                if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                    continue;
                }

                Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);
                if (internal == null) {
                    continue;
                }
                if (internal == mFacing) {
                    mCameraId = cameraId;
                    mCameraCharacteristics = characteristics;
                    return true;
                }
            }

            mCameraId = ids[1];
            mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
            Integer level = mCameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
            if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                return false;
            }

            Integer internal = mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
            if (internal == null) {
                return false;
            }
            mFacing = CameraCharacteristics.LENS_FACING_BACK;
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        return true;
    }

    @SuppressLint("MissingPermission")
    public void openCamera() {
        if (TextUtils.isEmpty(mCameraId)) {
            Log.e(TAG, "Open camera failed. No camera available");
            return;
        }

        try {
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock camera opening.");
            }
            //camera preview process,step1 打开camera
            mCameraManager.openCamera(mCameraId, mStateCallback, mCameraHandler);
        } catch (InterruptedException | CameraAccessException e) {
            Log.e(TAG, e.getMessage());
        }
    }

    private void closeCamera() {
        try {
            mCameraOpenCloseLock.acquire();
            if (mCaptureSession != null) {
                mCaptureSession.close();
                mCaptureSession = null;
            }
            if (mCameraDevice != null) {
                mCameraDevice.close();
                mCameraDevice = null;
            }
            if (mImageReader != null) {
                mImageReader.close();
                mImageReader = null;
            }
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera closing.", e);
        } finally {
            mCameraOpenCloseLock.release();
        }
    }

    private void startCaptureSession() {
        mState = State.STATE_PREVIEW;
        if (mCameraDevice == null) {
            return;
        }

        if ((mImageReader != null || mSurfaceTexture != null)) {
            try {
                //camera preview process,step3 创建一个 CaptureRequest.Builder,templateType来区分是拍照还是预览
                mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                //camera preview process,step4 将显示预览用的surface的实例传入,即将显示预览用的 surface 的实例,作为一个显示层添加到该 请求的目标列表中
                mPreviewRequestBuilder.addTarget(mImageReader.getSurface());
                List<Surface> surfaceList = Arrays.asList(mImageReader.getSurface());
                if (mSurfaceTexture != null) {
                    mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
                    Surface surface = new Surface(mSurfaceTexture);
                    mPreviewRequestBuilder.addTarget(mImageReader.getSurface());
                    mPreviewRequestBuilder.addTarget(surface);
                    //camera preview process,step5 将显示预览用的surface的实例传入,即将显示预览用的surface的实例,作为一个显示层添加到该请求的目标列表中
                    surfaceList = Arrays.asList(surface, mImageReader.getSurface());
                }

                Range<Integer>[] fpsRanges = mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
                Log.d("DEBUG", "##### fpsRange: " + Arrays.toString(fpsRanges));
                //camera preview process,step6 & 7
                // 6 执行createCaptureSession方法
                // 7 参数中实例化 CameraCaptureSession.stateCallback,并重写 onConfigured 方法
                mCameraDevice.createCaptureSession(surfaceList, new CameraCaptureSession.StateCallback() {
                    @Override
                    public void onConfigured(CameraCaptureSession session) {
                        if (mCameraDevice == null) return;
                        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);
                        mCaptureSession = session;
                        try {
                            if (mCaptureSession != null)
                                //camera preview process,step8 用 CameraCaptureSession.setRepeatingRequest()方法创建预览
                                mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mCameraHandler);
                        } catch (CameraAccessException | IllegalArgumentException | IllegalStateException | NullPointerException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onConfigureFailed(CameraCaptureSession session) {
                        Log.e(TAG, "Failed to configure capture session");
                    }

                    @Override
                    public void onClosed(CameraCaptureSession session) {
                        if (mCaptureSession != null && mCaptureSession.equals(session)) {
                            mCaptureSession = null;
                        }
                    }
                }, mCameraHandler);
            } catch (CameraAccessException e) {
                e.printStackTrace();
                Log.e(TAG, e.getMessage());
            } catch (IllegalStateException e) {
                stopPreview();
                startPreview();
            } catch (UnsupportedOperationException e) {
                e.printStackTrace();
                Log.e(TAG, e.getMessage());
            }
        }
    }

    public void captureStillPicture() {
        try {
            Log.d(TAG,"captureStillPicture");
            mState = State.STATE_CAPTURE;
            if (mCameraDevice == null) {
                return;
            }
            // camera capture process,step1 创建作为拍照的CaptureRequest.Builder
            mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            // camera capture process,step2 将imageReader的surface作为CaptureRequest.Builder的目标
            mCaptureRequestBuilder.addTarget(mImageReader.getSurface());
            // 设置自动对焦模式
            mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            // 设置自动曝光模式
            mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
            // 设置为自动模式
            mCaptureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
            // 设置摄像头旋转角度
            mCaptureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, Surface.ROTATION_0);
            // 停止连续取景
            mCaptureSession.stopRepeating();
            // camera capture process,step5 &6 捕获静态图像,结束后执行onCaptureCompleted
            mCaptureSession.capture(mCaptureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() {
                @Override// 拍照完成时激发该方法
                public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
                    Log.d(TAG,"onCaptureCompleted");
                    startCaptureSession();
                }
            }, mCameraHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    //camera capture progress,step4 保存图片相关操作
    public static class ImageSaver implements Runnable {
        private final Image mImage;
        private final File mFile;
        Context mContext;

        ImageSaver(Context context,Image image, File file) {
            mContext = context;
            mImage = image;
            mFile = file;
        }

        @Override
        public void run() {
            Log.d(TAG,"take picture Image Run");
            ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.DESCRIPTION, "This is an qr image");
            values.put(MediaStore.Images.Media.DISPLAY_NAME, mFile.getName());
            values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
            values.put(MediaStore.Images.Media.TITLE, "Image.jpg");
            values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/");
            Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            ContentResolver resolver = mContext.getContentResolver();
            Uri insertUri = resolver.insert(external, values);
            OutputStream os = null;
            try {
                if (insertUri != null) {
                    os = resolver.openOutputStream(insertUri);
                }
                if (os != null) {
                    os.write(bytes);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                mImage.close();
                try {
                    if(os!=null) {
                        os.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public interface FrameCallback {
        void onFrame(Image data);
    }

    public Size getPreviewSize() {
        return mPreviewSize;
    }

    public void setPreviewSize(Size previewSize) {
        mPreviewSize = previewSize;
    }

    public FrameCallback getFrameCallback() {
        return (FrameCallback) mFrameCallback;
    }

    public void setFrameCallback(FrameCallback frameCallback) {
        mFrameCallback = (Choreographer.FrameCallback) frameCallback;
    }

    public void setSurfaceTexture(SurfaceTexture surfaceTexture) {
        mSurfaceTexture = surfaceTexture;
    }
}

Activity UI相关代码如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private ImageButton mTakePictureBtn;
    private CameraCoreManager manager;
    private TextureView mTextureView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera);
        manager = new CameraCoreManager(this);
        mTakePictureBtn = findViewById(R.id.camera_take_picture);
        mTakePictureBtn.setOnClickListener(this);
        mTextureView = findViewById(R.id.texture_view);
    }

    @Override
    public void onResume() {
        super.onResume();
        mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
                manager.setSurfaceTexture(surface);
                manager.startPreview();
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surface) {
            }
        });
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        manager.stopPreview();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.camera_take_picture:
                //take picture
                break;
        }
    }
}

2.2 layout文件

这里涉及布局文件主要为activity_camera.xml,xml文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"

android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/gray"
    android:gravity="center"
    android:orientation="horizontal"
    android:padding="20dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent">

    <ImageButton
        android:id="@+id/camera_take_picture"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="@drawable/ic_camera" />
</LinearLayout>

<TextureView
    android:id="@+id/texture_view"
    android:layout_width="320dp"
    android:layout_height="180dp"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图王大胜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值