当前位置: 首页 > news >正文

Android camera2

一、序言

为了对阶段性的知识积累、方便以后调查问题,特做此文档!
将以camera app 使用camera2 api进行分析。
(1)、打开相机 openCamera
(2)、创建会话 createCaptureSession
(3)、开始预览 setRepeatingRequest
(4)、停止预览 stopRepeating
(5)、关闭相机 closeCamera

二、打开相机

1、camera app在使用camera2 api的第一步就是调用cameraManager的openCamera接口此接口有三个参数

在这里插入图片描述

2、调用时序

2.1 调用openCamera接口

在这里插入图片描述

2.2 内部调用到openCameraDeviceUserAsync

调用内部方法getCameraCharacteristics查询camera设备的相关属性,这里会通过binder调用到CameraService(c++), 主要是camera 的前后朝向、图像显示角度(0度、90度、180度、270度)

在这里插入图片描述
new CameraDeviceImpl 的时候将一些变量保存在该对象内,这个CameraDeviceImpl会返回一个CameraDeviceCallbacks后面会将这个callback通过CameraService的接口connect()给到CameraDeviceClientBase, 后续不管是预览、录制、拍照存在帧相关的error都会通过CameraDeviceCallbacks --> CaptureCallback
在这里插入图片描述接着去new client
在这里插入图片描述走到makeClient的方法里面,会根据camera api的版本选择new 不同的client
在这里插入图片描述

new CameraDeviceClient -> new Camera2ClientBase -> new CameraDeviceClientBase() -> new BasicClient(), 在Camera2ClientBase的构造函数里面还会new Camera3Device

在这里插入图片描述
在这里插入图片描述
等new client结束后就会去调用initialize
在这里插入图片描述
CameraDeviceClient::initialize() -> Camera2ClientBase::initialize() -> Camera2ClientBase::initializeImpl() -> Camera3Device::initialize -> CameraProviderManager::openSession, CameraProviderManager类可以理解为hal camera的代理类

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Camera3Device持有CameraProviderManager对象,而CameraProviderManager可以理解native和hal camera直接的桥梁,它会枚举出camera provider以及这些provider提供的摄像头设备,并提供方法去访问它们.
在这里插入图片描述
在这里插入图片描述
这里必须要插播一下CameraProviderManager的实例化和初始化了,如下图在CameraService 被new的时候就会自动走进onFirstRef(), 然后在该方法里面去new CameraProviderManager,紧接着调用mCameraProviderManager的initialize()方法,initialize()第二个参数是默认参数,直接赋值了HardwareServiceInteractionProxy的对象sHardwareServiceInteractionProxy, HardwareServiceInteractionProxy有两个方法,一个是registerForNotifications(), 一个是getService(), 这里getService()会拿到进程android.hardware.camera.provider@2.4-service中CameraProvider类的远端代理,而CameraProvider中的initialize()方法在CameraProvider的结构体中会被调用,在CameraProvider::initialize() 会去通过hw_get_module()方法打开动态库(camera*.so), 然后CameraProvider::
CameraModule中的camera_module_t *mModule 持有这个真正实现操作camera device的so句柄。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接着看addDevice方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
好了插播到此结束,接着上面的2.15 deviceInfo3->mInterface->open, mInterface就是CameraDevice,并且持有CameraModule,那么这里就会调用到CameraModule::open,紧接着调用到hal层真正实现者v4l2_camera_hal中的open

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、创建会话

1、在opencamera成功后,就会返回一个对象CameraDeviceImpl,后面在创建的会话和请求预览的时候都会调用该对象的方法。

2、创建会话会调用CameraDeviceImpl::createCaptureSession,分别有三个参数

在这里插入图片描述
3、创建会话代码流程分析
frameworks/base/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java

    // 当open camera成功之后,就会返回CameraDeviceImpl对象,然后在opened回调里面去创建会话,// 需要传进来两个surface,一个用于拍照,一个用于预览public void createCaptureSession(List<Surface> outputs,CameraCaptureSession.StateCallback callback, Handler handler)throws CameraAccessException {List<OutputConfiguration> outConfigurations = new ArrayList<>(outputs.size());for (Surface surface : outputs) {outConfigurations.add(new OutputConfiguration(surface));}createCaptureSessionInternal(null, outConfigurations, callback,checkAndWrapHandler(handler), /*operatingMode*/ICameraDeviceUser.NORMAL_MODE,/*sessionParams*/ null);}

frameworks/base/core/java/android/hardware/camera2/params/OutputConfiguration.java

    public OutputConfiguration(@NonNull Surface surface) {this(SURFACE_GROUP_ID_NONE, surface, ROTATION_0);}@SystemApipublic OutputConfiguration(int surfaceGroupId, @NonNull Surface surface, int rotation) {checkNotNull(surface, "Surface must not be null");checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");mSurfaceGroupId = surfaceGroupId;mSurfaceType = SURFACE_TYPE_UNKNOWN;mSurfaces = new ArrayList<Surface>(); // 有效值其实只有surfacemSurfaces.add(surface);mRotation = rotation;mConfiguredSize = SurfaceUtils.getSurfaceSize(surface);mConfiguredFormat = SurfaceUtils.getSurfaceFormat(surface);mConfiguredDataspace = SurfaceUtils.getSurfaceDataspace(surface);mConfiguredGenerationId = surface.getGenerationId();mIsDeferredConfig = false;mIsShared = false;mPhysicalCameraId = null;mIsMultiResolution = false;mSensorPixelModesUsed = new ArrayList<Integer>();}

frameworks/base/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
紧接着上面的createCaptureSessionInternal

private void createCaptureSessionInternal(InputConfiguration inputConfig,List<OutputConfiguration> outputConfigurations,CameraCaptureSession.StateCallback callback, Executor executor,int operatingMode, CaptureRequest sessionParams) throws CameraAccessException {.......try {// configure streams and then block until IDLE// 这里开始创建streamsconfigureSuccess = configureStreamsChecked(inputConfig, outputConfigurations,operatingMode, sessionParams, createSessionStartTime);if (configureSuccess == true && inputConfig != null) {input = mRemoteDevice.getInputSurface();}} catch (CameraAccessException e) {configureSuccess = false;pendingException = e;input = null;if (DEBUG) {Log.v(TAG, "createCaptureSession - failed with exception ", e);}}// Fire onConfigured if configureOutputs succeeded, fire onConfigureFailed otherwise.CameraCaptureSessionCore newSession = null;if (isConstrainedHighSpeed) {ArrayList<Surface> surfaces = new ArrayList<>(outputConfigurations.size());for (OutputConfiguration outConfig : outputConfigurations) {surfaces.add(outConfig.getSurface());}StreamConfigurationMap config =getCharacteristics().get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);SurfaceUtils.checkConstrainedHighSpeedSurfaces(surfaces, /*fpsRange*/null, config);newSession = new CameraConstrainedHighSpeedCaptureSessionImpl(mNextSessionId++,callback, executor, this, mDeviceExecutor, configureSuccess,mCharacteristics);} else {// 上面创建了stream之后就会去创建session, 后面又用session对象去请求预览camera// 创建session就是直接一个java的对象newSession = new CamerraCaptureSessionImpl(mNextSessionId++, input,callback, executor, this, mDeviceExecutor, configureSuccess);}// TODO: wait until current session closes, then create the new sessionmCurrentSession = newSession;if (pendingException != null) {throw pendingException;}mSessionStateCallback = mCurrentSession.getDeviceStateCallback();}}

构建Streams

 public boolean configureStreamsChecked(InputConfiguration inputConfig,List<OutputConfiguration> outputs, int operatingMode, CaptureRequest sessionParams,long createSessionStartTime)throws CameraAccessException {// Treat a null input the same an empty listif (outputs == null) {outputs = new ArrayList<OutputConfiguration>();}if (outputs.size() == 0 && inputConfig != null) {throw new IllegalArgumentException("cannot configure an input stream without " +"any output streams");}// 传进来的inputConfig就是 nullcheckInputConfiguration(inputConfig);boolean success = false;synchronized(mInterfaceLock) {checkIfCameraClosedOrInError();// Streams to createHashSet<OutputConfiguration> addSet = new HashSet<OutputConfiguration>(outputs);// Streams to deleteList<Integer> deleteList = new ArrayList<Integer>();// Determine which streams need to be created, which to be deleted// 这里新创建的CameraDeviceImpl 对象 mConfiguredOutputs.size() 都是0for (int i = 0; i < mConfiguredOutputs.size(); ++i) {int streamId = mConfiguredOutputs.keyAt(i);OutputConfiguration outConfig = mConfiguredOutputs.valueAt(i);if (!outputs.contains(outConfig) || outConfig.isDeferredConfiguration()) {// Always delete the deferred output configuration when the session// is created, as the deferred output configuration doesn't have unique surface// related identifies.deleteList.add(streamId);} else {addSet.remove(outConfig);  // Don't create a stream previously created}}mDeviceExecutor.execute(mCallOnBusy);// 这里新创建的CameraDeviceImpl 对象, mRepeatingRequestId == REQUEST_ID_NONE , // stopRepeating()等于什么都没有做stopRepeating();try {waitUntilIdle();// 调到 CameraDeviceClient::beginConfigure 中这里是个空实现,什么都没有做mRemoteDevice.beginConfigure();......// Delete all streams first (to free up HW resources)for (Integer streamId : deleteList) {mRemoteDevice.deleteStream(streamId);mConfiguredOutputs.delete(streamId);}// Add all new streams// 根据新的OutputConfiguration创建streamfor (OutputConfiguration outConfig : outputs) {`if (addSet.contains(outConfig)) {// 这里创建了Camera3OutputStreamint streamId = mRemoteDevice.createStream(outConfig);mConfiguredOutputs.put(streamId, outConfig);}}int offlineStreamIds[];if (sessionParams != null) {// 这里将stream的一些配置给到hal层,最终会调用到V4L2Camera::setupStreamsofflineStreamIds = mRemoteDevice.endConfigure(operatingMode,sessionParams.getNativeCopy(), createSessionStartTime);} else {offlineStreamIds = mRemoteDevice.endConfigure(operatingMode, null,createSessionStartTime);}......success = true;} catch (IllegalArgumentException e) {// OK. camera service can reject stream config if it's not supported by HAL// This is only the result of a programmer misusing the camera2 api.Log.w(TAG, "Stream configuration failed due to: " + e.getMessage());return false;} catch (CameraAccessException e) {if (e.getReason() == CameraAccessException.CAMERA_IN_USE) {throw new IllegalStateException("The camera is currently busy." +" You must wait until the previous operation completes.", e);}throw e;} finally {if (success && outputs.size() > 0) {mDeviceExecutor.execute(mCallOnIdle);} else {// Always return to the 'unconfigured' state if we didn't hit a fatal errormDeviceExecutor.execute(mCallOnUnconfigured);}}}return success;}

frameworks/av/services/camera/libcameraservice/api2/CameraDeviceClient
根据outputConfiguration去创建stream


binder::Status CameraDeviceClient::createStream(const hardware::camera2::params::OutputConfiguration &outputConfiguration,/*out*/int32_t* newStreamId) {ATRACE_CALL();binder::Status res;if (!(res = checkPidStatus(__FUNCTION__)).isOk()) return res;Mutex::Autolock icl(mBinderSerializationLock);// outputConfiguration是持有surface, surface中持有graphicBufferProducer,这个producer就是// 给到camera hal去填充camera数据的const std::vector<sp<IGraphicBufferProducer>>& bufferProducers =outputConfiguration.getGraphicBufferProducers();size_t numBufferProducers = bufferProducers.size();bool deferredConsumer = outputConfiguration.isDeferred();bool isShared = outputConfiguration.isShared();String8 physicalCameraId = String8(outputConfiguration.getPhysicalCameraId());bool deferredConsumerOnly = deferredConsumer && numBufferProducers == 0;bool isMultiResolution = outputConfiguration.isMultiResolution();..........std::vector<sp<Surface>> surfaces;std::vector<sp<IBinder>> binders;..........OutputStreamInfo streamInfo;bool isStreamInfoValid = false;const std::vector<int32_t> &sensorPixelModesUsed =outputConfiguration.getSensorPixelModesUsed();for (auto& bufferProducer : bufferProducers) {// Don't create multiple streams for the same target surface// 一个stream 可以对应多个surface,但是一个surface只能对应一个streamsp<IBinder> binder = IInterface::asBinder(bufferProducer);ssize_t index = mStreamMap.indexOfKey(binder);// if 走进去说明这个surface 已经对应了 stream,之前已经保存在了mStreamMap中if (index != NAME_NOT_FOUND) {String8 msg = String8::format("Camera %s: Surface already has a stream created for it ""(ID %zd)", mCameraIdStr.string(), index);ALOGW("%s: %s", __FUNCTION__, msg.string());return STATUS_ERROR(CameraService::ERROR_ALREADY_EXISTS, msg.string());}// 根据前面app申请的surface,获取其中的bufferProducer,然后去创建streamInfo和 c++ surface,// 并返回sp<Surface> surface;res = SessionConfigurationUtils::createSurfaceFromGbp(streamInfo,isStreamInfoValid, surface, bufferProducer, mCameraIdStr,mDevice->infoPhysical(physicalCameraId), sensorPixelModesUsed);.......binders.push_back(IInterface::asBinder(bufferProducer));surfaces.push_back(surface);}// If mOverrideForPerfClass is true, do not fail createStream() for small// JPEG sizes because existing createSurfaceFromGbp() logic will find the// closest possible supported size.int streamId = camera3::CAMERA3_STREAM_ID_INVALID;std::vector<int> surfaceIds;......} else {// 这里走到Camera3Device中,注意这里的streamId、surfaceIds是要在createStream进行赋值的,而surfaces给到Camera3OutputStream// 中,createStream 创建一个Camera3OutputStream 就会streamId++err = mDevice->createStream(surfaces, deferredConsumer, streamInfo.width,streamInfo.height, streamInfo.format, streamInfo.dataSpace,static_cast<camera_stream_rotation_t>(outputConfiguration.getRotation()),&streamId, physicalCameraId, streamInfo.sensorPixelModesUsed, &surfaceIds,outputConfiguration.getSurfaceSetID(), isShared, isMultiResolution);}if (err != OK) {......} else {int i = 0;// binders 中对应的是每个surface的bufferProducer,即可以理解binder中存在的就是surfacefor (auto& binder : binders) {ALOGV("%s: mStreamMap add binder %p streamId %d, surfaceId %d",__FUNCTION__, binder.get(), streamId, i);// streamMap 一个surface的bufferProducer, 对应一个StreamSurfaceId对象,// 该对象又存一个streamId和对应的surfaceIds[i], 这个streamId就是创建第几个// Camera3OutputStream,surfaceIds[i] 存在的都是0;mStreamMap.add(binder, StreamSurfaceId(streamId, surfaceIds[i]));i++;}// 一个streamId对应一个outputConfigurationmConfiguredOutputs.add(streamId, outputConfiguration);// 每个Camera3OutputStream 和 streamInfo对应关系mStreamInfoMap[streamId] = streamInfo;ALOGV("%s: Camera %s: Successfully created a new stream ID %d for output surface"" (%d x %d) with format 0x%x.",__FUNCTION__, mCameraIdStr.string(), streamId, streamInfo.width,streamInfo.height, streamInfo.format);// Set transform flags to ensure preview to be rotated correctly.res = setStreamTransformLocked(streamId);// Fill in mHighResolutionCameraIdToStreamIdSet mapconst String8 &cameraIdUsed =physicalCameraId.size() != 0 ? physicalCameraId : mCameraIdStr;const char *cameraIdUsedCStr = cameraIdUsed.string();// Only needed for high resolution sensorsif (mHighResolutionSensors.find(cameraIdUsedCStr) !=mHighResolutionSensors.end()) {mHighResolutionCameraIdToStreamIdSet[cameraIdUsedCStr].insert(streamId);}// 这个返回给app*newStreamId = streamId;}return res;
}

frameworks/av/services/camera/libcameraservice/utils/SessionConfigurationUtils.cpp

// 构建OutputStreamInfo,从app 申请的surface 获取wdith、height、fromat等信息 
binder::Status SessionConfigurationUtils::createSurfaceFromGbp(OutputStreamInfo& streamInfo, bool isStreamInfoValid,sp<Surface>& surface, const sp<IGraphicBufferProducer>& gbp,const String8 &logicalCameraId, const CameraMetadata &physicalCameraMetadata,const std::vector<int32_t> &sensorPixelModesUsed){.......// 其实前面的surface 中的gbp,new 一个c++ 层的Surface对象,C++ Surface 继承自ANativeWindowsurface = new Surface(gbp, useAsync);ANativeWindow *anw = surface.get();int width, height, format;android_dataspace dataSpace;// 从ANativeWindow 中获取surface的width、height、format等,if ((err = anw->query(anw, NATIVE_WINDOW_WIDTH, &width)) != OK) {String8 msg = String8::format("Camera %s: Failed to query Surface width: %s (%d)",logicalCameraId.string(), strerror(-err), err);ALOGE("%s: %s", __FUNCTION__, msg.string());return STATUS_ERROR(CameraService::ERROR_INVALID_OPERATION, msg.string());}if ((err = anw->query(anw, NATIVE_WINDOW_HEIGHT, &height)) != OK) {String8 msg = String8::format("Camera %s: Failed to query Surface height: %s (%d)",logicalCameraId.string(), strerror(-err), err);ALOGE("%s: %s", __FUNCTION__, msg.string());return STATUS_ERROR(CameraService::ERROR_INVALID_OPERATION, msg.string());}if ((err = anw->query(anw, NATIVE_WINDOW_FORMAT, &format)) != OK) {String8 msg = String8::format("Camera %s: Failed to query Surface format: %s (%d)",logicalCameraId.string(), strerror(-err), err);ALOGE("%s: %s", __FUNCTION__, msg.string());return STATUS_ERROR(CameraService::ERROR_INVALID_OPERATION, msg.string());}if ((err = anw->query(anw, NATIVE_WINDOW_DEFAULT_DATASPACE,reinterpret_cast<int*>(&dataSpace))) != OK) {String8 msg = String8::format("Camera %s: Failed to query Surface dataspace: %s (%d)",logicalCameraId.string(), strerror(-err), err);ALOGE("%s: %s", __FUNCTION__, msg.string());return STATUS_ERROR(CameraService::ERROR_INVALID_OPERATION, msg.string());}......// isStreamInfoValid 传进来就是false,然后将上面的信息给到streaminfoif (!isStreamInfoValid) {streamInfo.width = width;streamInfo.height = height;streamInfo.format = format;streamInfo.dataSpace = dataSpace;streamInfo.consumerUsage = consumerUsage;streamInfo.sensorPixelModesUsed = overriddenSensorPixelModes;return binder::Status::ok();}......}return binder::Status::ok();
}

frameworks/av/services/camera/libcameraservice/device3/Camera3Device.cpp

        bool hasDeferredConsumer, uint32_t width, uint32_t height, int format,android_dataspace dataSpace, camera_stream_rotation_t rotation, int *id,const String8& physicalCameraId, const std::unordered_set<int32_t> &sensorPixelModesUsed,std::vector<int> *surfaceIds, int streamSetId, bool isShared, bool isMultiResolution,uint64_t consumerUsage) {......sp<Camera3OutputStream> newStream;......if (format == HAL_PIXEL_FORMAT_BLOB) {......} else {// new 一个 Camera3OutputStreamnewStream = new Camera3OutputStream(mNextStreamId, consumers[0],width, height, format, dataSpace, rotation,mTimestampOffset, physicalCameraId, sensorPixelModesUsed, streamSetId,isMultiResolution);}size_t consumerCount = consumers.size();for (size_t i = 0; i < consumerCount; i++) {// 这里Camera3OutputStream::getSurfaceId 的实现就是返回0;int id = newStream->getSurfaceId(consumers[i]);if (id < 0) {SET_ERR_L("Invalid surface id");return BAD_VALUE;}// 这里将前面传进来的surface其中的Id都存到surfaceIds中 if (surfaceIds != nullptr) {surfaceIds->push_back(id);}}newStream->setStatusTracker(mStatusTracker);newStream->setBufferManager(mBufferManager);newStream->setImageDumpMask(mImageDumpMask);res = mOutputStreams.add(mNextStreamId, newStream);if (res < 0) {SET_ERR_L("Can't add new stream to set: %s (%d)", strerror(-res), res);return res;}mSessionStatsBuilder.addStream(mNextStreamId);*id = mNextStreamId++;mNeedConfig = true;// Continue captures if active at startif (wasActive) {ALOGV("%s: Restarting activity to reconfigure streams", __FUNCTION__);// Reuse current operating mode and session parameters for new stream configres = configureStreamsLocked(mOperatingMode, mSessionParams);if (res != OK) {CLOGE("Can't reconfigure device for new stream %d: %s (%d)",mNextStreamId, strerror(-res), res);return res;}internalResumeLocked();}ALOGV("Camera %s: Created new stream", mId.string());return OK;
}

frameworks/av/services/camera/libcameraservice/device3/Camera3OutputStream.cpp

Camera3OutputStream::Camera3OutputStream(int id,sp<Surface> consumer,uint32_t width, uint32_t height, int format,android_dataspace dataSpace, camera_stream_rotation_t rotation,nsecs_t timestampOffset, const String8& physicalCameraId,const std::unordered_set<int32_t> &sensorPixelModesUsed,int setId, bool isMultiResolution) :Camera3IOStreamBase(id, CAMERA_STREAM_OUTPUT, width, height,/*maxSize*/0, format, dataSpace, rotation,physicalCameraId, sensorPixelModesUsed, setId, isMultiResolution),mConsumer(consumer),mTransform(0),mTraceFirstBuffer(true),mUseBufferManager(false),mTimestampOffset(timestampOffset),mConsumerUsage(0),mDropBuffers(false),mDequeueBufferLatency(kDequeueLatencyBinSize) {if (mConsumer == NULL) {ALOGE("%s: Consumer is NULL!", __FUNCTION__);mState = STATE_ERROR;}bool needsReleaseNotify = setId > CAMERA3_STREAM_SET_ID_INVALID;mBufferProducerListener = new BufferProducerListener(this, needsReleaseNotify);
}

时序图如下:
在这里插入图片描述

四、开始预览

1、从CameraManger -> openCamera() 获取CameraDeviceImpl ,然后调用CameraDeviceImpl的createCaptureSession的去创建会话,获取CameraCaptureSessionImpl对象,接着调用CameraCaptureSessionImpl的setRepeatingRequest去预览。

frameworks/base/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java

  public int setRepeatingRequest(CaptureRequest request, CaptureCallback callback,Handler handler) throws CameraAccessException {checkRepeatingRequest(request);synchronized (mDeviceImpl.mInterfaceLock) {checkNotClosed();handler = checkHandler(handler, callback);if (DEBUG) {Log.v(TAG, mIdString + "setRepeatingRequest - request " + request + ", callback " +callback + " handler" + " " + handler);}return addPendingSequence(mDeviceImpl.setRepeatingRequest(request,createCaptureCallbackProxy(handler, callback), mDeviceExecutor));}}

frameworks/base/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java

            Executor executor) throws CameraAccessException {List<CaptureRequest> requestList = new ArrayList<CaptureRequest>();requestList.add(request);return submitCaptureRequest(requestList, callback, executor, /*streaming*/true);}
 private int submitCaptureRequest(List<CaptureRequest> requestList, CaptureCallback callback,Executor executor, boolean repeating) throws CameraAccessException {......// 如果打开过了就先停掉,然后再打开if (repeating) {stopRepeating();}SubmitInfo requestInfo;CaptureRequest[] requestArray = requestList.toArray(new CaptureRequest[requestList.size()]);// Convert Surface to streamIdx and surfaceIdxfor (CaptureRequest request : requestArray) {request.convertSurfaceToStreamId(mConfiguredOutputs);}// 调到CameraDeviceClient的submitRequestListrequestInfo = mRemoteDevice.submitRequestList(requestArray, repeating);if (DEBUG) {Log.v(TAG, "last frame number " + requestInfo.getLastFrameNumber());}......if (repeating) {if (mRepeatingRequestId != REQUEST_ID_NONE) {checkEarlyTriggerSequenceCompleteLocked(mRepeatingRequestId,requestInfo.getLastFrameNumber(),mRepeatingRequestTypes);}mRepeatingRequestId = requestInfo.getRequestId();mRepeatingRequestTypes = getRequestTypes(requestArray);} else {mRequestLastFrameNumbersList.add(new RequestLastFrameNumbersHolder(requestList, requestInfo));}if (mIdle) {mDeviceExecutor.execute(mCallOnActive);}mIdle = false;return requestInfo.getRequestId();}}

frameworks/av/services/camera/libcameraservice/api2/CameraDeviceClient.cpp

binder::Status CameraDeviceClient::submitRequestList(const std::vector<hardware::camera2::CaptureRequest>& requests,bool streaming,/*out*/hardware::camera2::utils::SubmitInfo *submitInfo) {......List<const CameraDeviceBase::PhysicalCameraSettingsList> metadataRequestList;std::list<const SurfaceMap> surfaceMapList;submitInfo->mRequestId = mRequestIdCounter;uint32_t loopCounter = 0;// for (auto&& request: requests) {if (request.mIsReprocess) {if (!mInputStream.configured) {ALOGE("%s: Camera %s: no input stream is configured.", __FUNCTION__,mCameraIdStr.string());return STATUS_ERROR_FMT(CameraService::ERROR_ILLEGAL_ARGUMENT,"No input configured for camera %s but request is for reprocessing",mCameraIdStr.string());} else if (streaming) {ALOGE("%s: Camera %s: streaming reprocess requests not supported.", __FUNCTION__,mCameraIdStr.string());return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Repeating reprocess requests not supported");} else if (request.mPhysicalCameraSettings.size() > 1) {ALOGE("%s: Camera %s: reprocess requests not supported for ""multiple physical cameras.", __FUNCTION__,mCameraIdStr.string());return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Reprocess requests not supported for multiple cameras");}}if (request.mPhysicalCameraSettings.empty()) {ALOGE("%s: Camera %s: request doesn't contain any settings.", __FUNCTION__,mCameraIdStr.string());return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Request doesn't contain any settings");}//The first capture settings should always match the logical camera idString8 logicalId(request.mPhysicalCameraSettings.begin()->id.c_str());if (mDevice->getId() != logicalId) {ALOGE("%s: Camera %s: Invalid camera request settings.", __FUNCTION__,mCameraIdStr.string());return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Invalid camera request settings");}if (request.mSurfaceList.isEmpty() && request.mStreamIdxList.size() == 0) {ALOGE("%s: Camera %s: Requests must have at least one surface target. ""Rejecting request.", __FUNCTION__, mCameraIdStr.string());return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Request has no output targets");}/*** Write in the output stream IDs and map from stream ID to surface ID* which we calculate from the capture request's list of surface target*/SurfaceMap surfaceMap;Vector<int32_t> outputStreamIds;std::vector<std::string> requestedPhysicalIds;if (request.mSurfaceList.size() > 0) {for (const sp<Surface>& surface : request.mSurfaceList) {if (surface == 0) continue;int32_t streamId;sp<IGraphicBufferProducer> gbp = surface->getIGraphicBufferProducer();res = insertGbpLocked(gbp, &surfaceMap, &outputStreamIds, &streamId);if (!res.isOk()) {return res;}ssize_t index = mConfiguredOutputs.indexOfKey(streamId);if (index >= 0) {String8 requestedPhysicalId(mConfiguredOutputs.valueAt(index).getPhysicalCameraId());requestedPhysicalIds.push_back(requestedPhysicalId.string());} else {ALOGW("%s: Output stream Id not found among configured outputs!", __FUNCTION__);}}} else {for (size_t i = 0; i < request.mStreamIdxList.size(); i++) {int streamId = request.mStreamIdxList.itemAt(i);int surfaceIdx = request.mSurfaceIdxList.itemAt(i);ssize_t index = mConfiguredOutputs.indexOfKey(streamId);if (index < 0) {ALOGE("%s: Camera %s: Tried to submit a request with a surface that"" we have not called createStream on: stream %d",__FUNCTION__, mCameraIdStr.string(), streamId);return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Request targets Surface that is not part of current capture session");}const auto& gbps = mConfiguredOutputs.valueAt(index).getGraphicBufferProducers();if ((size_t)surfaceIdx >= gbps.size()) {ALOGE("%s: Camera %s: Tried to submit a request with a surface that"" we have not called createStream on: stream %d, surfaceIdx %d",__FUNCTION__, mCameraIdStr.string(), streamId, surfaceIdx);return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Request targets Surface has invalid surface index");}res = insertGbpLocked(gbps[surfaceIdx], &surfaceMap, &outputStreamIds, nullptr);if (!res.isOk()) {return res;}String8 requestedPhysicalId(mConfiguredOutputs.valueAt(index).getPhysicalCameraId());requestedPhysicalIds.push_back(requestedPhysicalId.string());}}CameraDeviceBase::PhysicalCameraSettingsList physicalSettingsList;for (const auto& it : request.mPhysicalCameraSettings) {if (it.settings.isEmpty()) {ALOGE("%s: Camera %s: Sent empty metadata packet. Rejecting request.",__FUNCTION__, mCameraIdStr.string());return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Request settings are empty");}// Check whether the physical / logical stream has settings// consistent with the sensor pixel mode(s) it was configured with.// mCameraIdToStreamSet will only have ids that are high resolutionconst auto streamIdSetIt = mHighResolutionCameraIdToStreamIdSet.find(it.id);if (streamIdSetIt != mHighResolutionCameraIdToStreamIdSet.end()) {std::list<int> streamIdsUsedInRequest = getIntersection(streamIdSetIt->second,outputStreamIds);if (!request.mIsReprocess &&!isSensorPixelModeConsistent(streamIdsUsedInRequest, it.settings)) {ALOGE("%s: Camera %s: Request settings CONTROL_SENSOR_PIXEL_MODE not ""consistent with configured streams. Rejecting request.",__FUNCTION__, it.id.c_str());return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Request settings CONTROL_SENSOR_PIXEL_MODE are not consistent with ""streams configured");}}String8 physicalId(it.id.c_str());bool hasTestPatternModePhysicalKey = std::find(mSupportedPhysicalRequestKeys.begin(),mSupportedPhysicalRequestKeys.end(), ANDROID_SENSOR_TEST_PATTERN_MODE) !=mSupportedPhysicalRequestKeys.end();bool hasTestPatternDataPhysicalKey = std::find(mSupportedPhysicalRequestKeys.begin(),mSupportedPhysicalRequestKeys.end(), ANDROID_SENSOR_TEST_PATTERN_DATA) !=mSupportedPhysicalRequestKeys.end();if (physicalId != mDevice->getId()) {auto found = std::find(requestedPhysicalIds.begin(), requestedPhysicalIds.end(),it.id);if (found == requestedPhysicalIds.end()) {ALOGE("%s: Camera %s: Physical camera id: %s not part of attached outputs.",__FUNCTION__, mCameraIdStr.string(), physicalId.string());return STATUS_ERROR(CameraService::ERROR_ILLEGAL_ARGUMENT,"Invalid physical camera id");}if (!mSupportedPhysicalRequestKeys.empty()) {// Filter out any unsupported physical request keys.CameraMetadata filteredParams(mSupportedPhysicalRequestKeys.size());camera_metadata_t *meta = const_cast<camera_metadata_t *>(filteredParams.getAndLock());set_camera_metadata_vendor_id(meta, mDevice->getVendorTagId());filteredParams.unlock(meta);for (const auto& keyIt : mSupportedPhysicalRequestKeys) {camera_metadata_ro_entry entry = it.settings.find(keyIt);if (entry.count > 0) {filteredParams.update(entry);}}physicalSettingsList.push_back({it.id, filteredParams,hasTestPatternModePhysicalKey, hasTestPatternDataPhysicalKey});}} else {physicalSettingsList.push_back({it.id, it.settings});}}if (!enforceRequestPermissions(physicalSettingsList.begin()->metadata)) {// Callee logsreturn STATUS_ERROR(CameraService::ERROR_PERMISSION_DENIED,"Caller does not have permission to change restricted controls");}physicalSettingsList.begin()->metadata.update(ANDROID_REQUEST_OUTPUT_STREAMS,&outputStreamIds[0], outputStreamIds.size());if (request.mIsReprocess) {physicalSettingsList.begin()->metadata.update(ANDROID_REQUEST_INPUT_STREAMS,&mInputStream.id, 1);}physicalSettingsList.begin()->metadata.update(ANDROID_REQUEST_ID,&(submitInfo->mRequestId), /*size*/1);loopCounter++; // loopCounter starts from 1ALOGV("%s: Camera %s: Creating request with ID %d (%d of %zu)",__FUNCTION__, mCameraIdStr.string(), submitInfo->mRequestId,loopCounter, requests.size());metadataRequestList.push_back(physicalSettingsList);surfaceMapList.push_back(surfaceMap);}mRequestIdCounter++;if (streaming) {// 预览走这里err = mDevice->setStreamingRequestList(metadataRequestList, surfaceMapList,&(submitInfo->mLastFrameNumber));} else {// 拍照走这里err = mDevice->captureList(metadataRequestList, surfaceMapList,&(submitInfo->mLastFrameNumber));}ALOGV("%s: Camera %s: End of function", __FUNCTION__, mCameraIdStr.string());return res;
}
status_t Camera3Device::setStreamingRequestList(const List<const PhysicalCameraSettingsList> &requestsList,const std::list<const SurfaceMap> &surfaceMaps, int64_t *lastFrameNumber) {ATRACE_CALL();return submitRequestsHelper(requestsList, surfaceMaps, /*repeating*/true, lastFrameNumber);
}status_t Camera3Device::submitRequestsHelper(const List<const PhysicalCameraSettingsList> &requests,const std::list<const SurfaceMap> &surfaceMaps,bool repeating,/*out*/int64_t *lastFrameNumber) {// 预览走这里if (repeating) {res = mRequestThread->setRepeatingRequests(requestList, lastFrameNumber);} else {res = mRequestThread->queueRequestList(requestList, lastFrameNumber);}......return res;
}status_t Camera3Device::RequestThread::setRepeatingRequests(const RequestList &requests,/*out*/int64_t *lastFrameNumber) {ATRACE_CALL();Mutex::Autolock l(mRequestLock);if (lastFrameNumber != NULL) {*lastFrameNumber = mRepeatingLastFrameNumber;}mRepeatingRequests.clear();mFirstRepeating = true;// 将requests请求都给到mRepeatingRequests里面mRepeatingRequests.insert(mRepeatingRequests.begin(),requests.begin(), requests.end());unpauseForNewRequests();mRepeatingLastFrameNumber = hardware::camera2::ICameraDeviceUser::NO_IN_FLIGHT_REPEATING_FRAMES;return OK;
}void Camera3Device::RequestThread::unpauseForNewRequests() {ATRACE_CALL();// With work to do, mark thread as unpaused.// If paused by request (setPaused), don't resume, to avoid// extra signaling/waiting overhead to waitUntilPaused// 主要是这里的signal然后,唤醒一个线程去进行发送请求给halmRequestSignal.signal();Mutex::Autolock p(mPauseLock);if (!mDoPause) {ALOGV("%s: RequestThread: Going active", __FUNCTION__);if (mPaused) {sp<StatusTracker> statusTracker = mStatusTracker.promote();if (statusTracker != 0) {statusTracker->markComponentActive(mStatusId);}}mPaused = false;}
}

mRequestSignal.signal()要去唤醒之前跑起来阻塞住的线程

在这里插入图片描述
下发请求
在这里插入图片描述
Threadloop 开始工作
在这里插入图片描述
等待的线程被打断开始获取buffer,通过processBatchCaptureRequests下发请求到camera hal
在这里插入图片描述
通过onResultAvailable 通知app camera data准备好了

五、关闭预览

停止预览app会调用接口CameraDeviceImpl的stopRepeating接口,通过trace可以看到如下调用在这里插入图片描述
CameraDeviceImpl::stopRepeating -> CameraDeviceClient::cancelRequest -> Camera3Device::clearStreamingRequest -> Camera3Device::RequestThread::clearRepeatingRequests->
Camera3Device::RequestThread::clearRepeatingRequestsLocked
会在clearRepeatingRequestsLocked中clear mRepeatingRequests List, 然后mRepeatingRequests为0后
Camera3Device 的threadloop 就会走到
在这里插入图片描述
然后停止获取buffer向camera hal发送

六、关闭相机

在这里插入图片描述
app调用close接口关闭camera,调用时序如上,在cameraserver中会退出相关的threadloop线程,在camera hal会正式做camera ioctrl close 销毁ais client 对应的camera context

相关文章:

Android camera2

一、序言 为了对阶段性的知识积累、方便以后调查问题&#xff0c;特做此文档&#xff01; 将以camera app 使用camera2 api进行分析。 (1)、打开相机 openCamera (2)、创建会话 createCaptureSession (3)、开始预览 setRepeatingRequest (4)、停止预览 stopRepeating (5)、关闭…...

nginx监控指标有哪些

Nginx 的监控指标可以帮助你了解服务器的性能、资源使用以及运行状态。下面是一些常见的 Nginx 监控指标&#xff0c;涵盖了访问、性能、资源使用等多个方面&#xff1a; 1. 访问量与请求处理 Active Connections&#xff08;活跃连接数&#xff09;&#xff1a;当前 Nginx 处…...

我谈正态分布——正态偏态

目录 pdf和cdf参数 标准正态分布期望和方差分布形态 3 σ 3\sigma 3σ原则 正态和偏态正态偏态瑞利分布偏度 (Skewness)峰度 (Kurtosis) 比较 正态分布的英文是Normal Distribution&#xff0c;normal是“正常”或“标准”的意思&#xff0c;中文翻译是正态&#xff0c;多完美的…...

如何使用uniswap v2 获取两个代币的交易对池子

在 Uniswap V2 中,获取两个代币的交易对池子(即 pair)可以通过以下步骤实现: 连接到 Uniswap V2 的合约:你需要与 Uniswap V2 的 Factory 合约进行交互,通过该合约来查找代币交易对。 获取交易对地址:Uniswap V2 Factory 合约提供了一个 getPair 函数,可以通过该函数查…...

CSS中常见的两列布局、三列布局、百分比和多行多列布局!

目录 一、两列布局 1、前言&#xff1a; 2. 两列布局的常见用法 两列布局的元素示例&#xff1a; 代码运行后如下&#xff1a; 二、三列布局 1.前言 2. 三列布局的常见用法 三列布局的元素示例&#xff1a; 代码运行后如下&#xff1a; 三、多行多列 1.前言 2&…...

GaussDB Ustore存储引擎解读

目录 一、数据库存储引擎 二、GaussDB Ustore存储引擎 总结 本文将介绍GaussDB中的Ustore存储引擎&#xff0c;包括Ustore的设计背景、特点介绍和适用业务场景等。 一、数据库存储引擎 数据库的存储引擎负责在内存和磁盘上存储、检索和管理数据&#xff0c;确保每个节点的…...

JAVA基础:数组 (习题笔记)

一&#xff0c;编码题 1&#xff0c;数组查找操作&#xff1a;定义一个长度为10 的一维字符串数组&#xff0c;在每一个元素存放一个单词&#xff1b;然后运行时从命令行输入一个单词&#xff0c;程序判断数组是否包含有这个单词&#xff0c;包含这个单词就打印出“Yes”&…...

VMWARE ESXI VMFS阵列故障 服务器数据恢复

1&#xff1a;河南用户一台DELL R740 3块2.4T硬盘组的RAID5&#xff0c;早期坏了一个盘没有及时更换&#xff0c;这次又坏了一个&#xff0c;导致整组RAID5处于数据丢失的状态&#xff0c; 2&#xff1a;该服务器装的是VMware ESXI 6.7&#xff0c;用户把3块硬盘寄过来进行数据…...

实时金融股票数据API接口websocket接入方法

一、使用websocket的协议提升传输速度 实时金融股票数据对于投资者和交易员来说至关重要。通过使用WebSocket接入方法&#xff0c;可以轻松获取实时金融股票类数据并及时做出决策。WebSocket是一种高效的双向通信协议&#xff0c;它允许数据的实时推送&#xff0c;避免了不断的…...

机器学习与成像技术

机器学习与成像技术 在科技日新月异的今天&#xff0c;机器学习与成像技术的结合正引领着智能视觉领域进入一个全新的发展阶段。这一结合不仅推动了图像识别、目标检测、视频分析等领域的快速发展&#xff0c;还深刻影响着医疗、安防、自动驾驶等多个行业。本文将从机器学习与…...

【系统架构设计师】预测试卷一:综合知识(75道选择题)

更多内容请见: 备考系统架构设计师-专栏介绍和目录 文章目录 【第1题】【第2题】【第3题】【第4题】【第5~6题】【第7题】【第8~10题】【第11题】【第12题】【第13题】【第14题】【第15题】【第16题】【第17~18题】【第19~20题】【第21~22题】【第23~24题】【第25~26题】【第2…...

【addRepository 在tomcat 8和tomcat 9的支持情况】

项目中涉及将远程下载的 jar包进行解密后加载到 tomcat 容器中。 File jarFile new File(fileUrl); String jarFileUrl jarFile.toURI().toURL().toString(); WebappClassLoader webLoader (WebappClassLoader) classLoader; webLoader.addRepository(jarFileUrl);在升级到 …...

2024网鼎杯web1+re2 wp

这两道题属于比较简单的&#xff0c;顺道说一下&#xff0c;今年的题有点抽象&#xff0c;web不是misc&#xff0c;re不是web的&#xff0c;也有可能时代在进步&#xff0c;现在要求全栈✌了吧 web1 最开始被强网的小浣熊带偏思路了&#xff0c;进来疯狂找sql注入&#xff0c…...

Python 自动化运维:安全与合规最佳实践

Python 自动化运维&#xff1a;安全与合规最佳实践 目录 &#x1f512; Python安全编程实践与最佳实践&#x1f511; 使用Hashlib与Cryptography进行数据加密&#x1f4ca; 安全审计与合规检查的重要性&#x1f50d; 处理敏感数据与隐私保护的方法 1. &#x1f512; Python安…...

I2S、PDM、PCM、TDM、DSM、DCODEC、VAD、SPDIF

I2S (Inter-IC Sound) 用途: 一种用于芯片之间传输音频数据的串行总线标准。特点: 常用于高质量音频设备,如DAC、ADC和音频编解码器。I2S主要传输PCM格式的音频数据。PDM (Pulse Density Modulation) 用途: 主要用于数字麦克风等设备,以简化硬件接口。特点: 使用脉冲密度来编…...

关于我的编程语言——C/C++——第四篇(深入1)

&#xff08;叠甲&#xff1a;如有侵权请联系&#xff0c;内容都是自己学习的总结&#xff0c;一定不全面&#xff0c;仅当互相交流&#xff08;轻点骂&#xff09;我也只是站在巨人肩膀上的一个小卡拉米&#xff0c;已老实&#xff0c;求放过&#xff09; 字符类型介绍 char…...

2025年上半年软考高级科目有哪些?附选科指南

新手在准备报考软考时&#xff0c;都会遇到这样的一个问题——科目这么多&#xff0c;我适合考什么&#xff1f;2025上半年软考高级有哪些科目可以报考&#xff1f;要想知道自己适合报什么科目&#xff0c;就需要了解每个科目是什么&#xff0c;考什么等一系列的问题&#xff0…...

线上查企业该用哪家平台?

在销售领域&#xff0c;经常会遇到电话接通率低的问题。这可能是因为许多电话号码来源于某些商业信息平台&#xff0c;这些号码可能已经被代账公司使用&#xff0c;或者已经被同行业多次联系过&#xff0c;导致接通率不高。为了解决这一问题&#xff0c;可以采用数据过滤服务&a…...

Metrix:实现CI服务器上的DORA指标自动化计算

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;Metrix项目是一个Go语言编写的工具&#xff0c;旨在自动化计算DevOps效能的关键指标——DORA指标&#xff0c;这些指标包括部署频率、前置时间、平均恢复时间和变更失败率。它通过集成到CI服务器如Jenkins或GitH…...

【STL_list 模拟】——打造属于自己的高效链表容器

一、list节点 ​ list是一个双向循环带头的链表&#xff0c;所以链表节点结构如下&#xff1a; template<class T>struct ListNode{T val;ListNode* next;ListNode* prve;ListNode(int x){val x;next prve this;}};二、list迭代器 2.1、list迭代器与vector迭代器区别…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统

医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上&#xff0c;开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识&#xff0c;在 vs 2017 平台上&#xff0c;进行 ASP.NET 应用程序和简易网站的开发&#xff1b;初步熟悉开发一…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件

在选煤厂、化工厂、钢铁厂等过程生产型企业&#xff0c;其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进&#xff0c;需提前预防假检、错检、漏检&#xff0c;推动智慧生产运维系统数据的流动和现场赋能应用。同时&#xff0c;…...

汽车生产虚拟实训中的技能提升与生产优化​

在制造业蓬勃发展的大背景下&#xff0c;虚拟教学实训宛如一颗璀璨的新星&#xff0c;正发挥着不可或缺且日益凸显的关键作用&#xff0c;源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例&#xff0c;汽车生产线上各类…...

GitHub 趋势日报 (2025年06月08日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

Java线上CPU飙高问题排查全指南

一、引言 在Java应用的线上运行环境中&#xff0c;CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时&#xff0c;通常会导致应用响应缓慢&#xff0c;甚至服务不可用&#xff0c;严重影响用户体验和业务运行。因此&#xff0c;掌握一套科学有效的CPU飙高问题排查方法&…...

佰力博科技与您探讨热释电测量的几种方法

热释电的测量主要涉及热释电系数的测定&#xff0c;这是表征热释电材料性能的重要参数。热释电系数的测量方法主要包括静态法、动态法和积分电荷法。其中&#xff0c;积分电荷法最为常用&#xff0c;其原理是通过测量在电容器上积累的热释电电荷&#xff0c;从而确定热释电系数…...

AI病理诊断七剑下天山,医疗未来触手可及

一、病理诊断困局&#xff1a;刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断"&#xff0c;医生需通过显微镜观察组织切片&#xff0c;在细胞迷宫中捕捉癌变信号。某省病理质控报告显示&#xff0c;基层医院误诊率达12%-15%&#xff0c;专家会诊…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

莫兰迪高级灰总结计划简约商务通用PPT模版

莫兰迪高级灰总结计划简约商务通用PPT模版&#xff0c;莫兰迪调色板清新简约工作汇报PPT模版&#xff0c;莫兰迪时尚风极简设计PPT模版&#xff0c;大学生毕业论文答辩PPT模版&#xff0c;莫兰迪配色总结计划简约商务通用PPT模版&#xff0c;莫兰迪商务汇报PPT模版&#xff0c;…...