赞
踩
本示例主要展示了相机的相关功能 接口实现相机的预览拍照功能。
使用说明
/* * Copyright (c) 2024 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { camera } from '@kit.CameraKit'; import { media } from '@kit.MediaKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { JSON } from '@kit.ArkTS'; import { fileIo } from '@kit.CoreFileKit'; import { GlobalContext } from '../common/utils/GlobalContext'; import Logger from '../common/utils/Logger'; import { Constants } from '../common/Constants'; const TAG: string = 'CameraService'; export class SliderValue { min: number = 1; max: number = 6; step: number = 0.1; } class CameraService { private cameraManager: camera.CameraManager | undefined = undefined; private cameras: Array<camera.CameraDevice> | Array<camera.CameraDevice> = []; private cameraInput: camera.CameraInput | undefined = undefined; private previewOutput: camera.PreviewOutput | undefined = undefined; private photoOutput: camera.PhotoOutput | undefined = undefined; private videoOutput: camera.VideoOutput | undefined = undefined; private avRecorder: media.AVRecorder | undefined = undefined; private session: camera.PhotoSession | camera.VideoSession | undefined = undefined; private handlePhotoAssetCb: (photoAsset: photoAccessHelper.PhotoAsset) => void = () => { }; private curCameraDevice: camera.CameraDevice | undefined = undefined; private isRecording: boolean = false; // 推荐拍照分辨率之一 private photoProfileObj: camera.Profile = { format: 2000, size: { width: 1920, height: 1080 } }; // 推荐预览分辨率之一 private previewProfileObj: camera.Profile = { format: 1003, size: { width: 1920, height: 1080 } }; // 推荐录像分辨率之一 private videoProfileObj: camera.VideoProfile = { format: 1003, size: { width: 1920, height: 1080 }, frameRateRange: { min: 30, max: 60 } }; private curSceneMode: camera.SceneMode = camera.SceneMode.NORMAL_PHOTO; constructor() { } setSavePictureCallback(callback: (photoAsset: photoAccessHelper.PhotoAsset) => void): void { this.handlePhotoAssetCb = callback; } setSceneMode(sceneMode: camera.SceneMode): void { this.curSceneMode = sceneMode; } getSceneMode(): camera.SceneMode { return this.curSceneMode; } getPreviewProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined { let previewProfiles = cameraOutputCapability.previewProfiles; if (previewProfiles.length < 1) { return undefined; } let index = previewProfiles.findIndex((previewProfile: camera.Profile) => { return previewProfile.size.width === this.previewProfileObj.size.width && previewProfile.size.height === this.previewProfileObj.size.height && previewProfile.format === this.previewProfileObj.format; }); if (index === -1) { return undefined; } return previewProfiles[index]; } getPhotoProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined { let photoProfiles = cameraOutputCapability.photoProfiles; if (photoProfiles.length < 1) { return undefined; } let index = photoProfiles.findIndex((photoProfile: camera.Profile) => { return photoProfile.size.width === this.photoProfileObj.size.width && photoProfile.size.height === this.photoProfileObj.size.height && photoProfile.format === this.photoProfileObj.format; }); if (index === -1) { return undefined; } return photoProfiles[index]; } getVideoProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.VideoProfile | undefined { let videoProfiles = cameraOutputCapability.videoProfiles; if (videoProfiles.length < 1) { return undefined; } for (let i = 0; i < videoProfiles.length; i++) { Logger.info(TAG, `getVideoProfile: ${JSON.stringify(videoProfiles[i])}`); } let index = videoProfiles.findIndex((videoProfile: camera.VideoProfile) => { return videoProfile.size.width === this.videoProfileObj.size.width && videoProfile.size.height === this.videoProfileObj.size.height && videoProfile.format === this.videoProfileObj.format && videoProfile.frameRateRange.min <= Constants.MAX_VIDEO_FRAME && videoProfile.frameRateRange.max <= Constants.MAX_VIDEO_FRAME; }); if (index === -1) { return undefined; } return videoProfiles[index]; } isSupportedSceneMode(cameraManager: camera.CameraManager, cameraDevice: camera.CameraDevice): boolean { let sceneModes = cameraManager.getSupportedSceneModes(cameraDevice); if (sceneModes === undefined) { return false; } let index = sceneModes.findIndex((sceneMode: camera.SceneMode) => { return sceneMode === this.curSceneMode; }); if (index === -1) { return false; } return true; } /** * 初始化相机功能 * @param surfaceId - Surface 的 ID * @param cameraDeviceIndex - 相机设备索引 * @returns 无返回值 */ async initCamera(surfaceId: string, cameraDeviceIndex: number): Promise<void> { Logger.debug(TAG, `initCamera cameraDeviceIndex: ${cameraDeviceIndex}`); try { await this.releaseCamera(); // 获取相机管理器实例 this.cameraManager = this.getCameraManagerFn(); if (this.cameraManager === undefined) { Logger.error(TAG, 'cameraManager is undefined'); return; } // 获取支持指定的相机设备对象 this.cameras = this.getSupportedCamerasFn(this.cameraManager); if (this.cameras.length < 1 || this.cameras.length < cameraDeviceIndex + 1) { return; } this.curCameraDevice = this.cameras[cameraDeviceIndex]; let isSupported = this.isSupportedSceneMode(this.cameraManager, this.curCameraDevice); if (!isSupported) { Logger.error(TAG, 'The current scene mode is not supported.'); return; } let cameraOutputCapability = this.cameraManager.getSupportedOutputCapability(this.curCameraDevice, this.curSceneMode); let previewProfile = this.getPreviewProfile(cameraOutputCapability); if (previewProfile === undefined) { Logger.error(TAG, 'The resolution of the current preview stream is not supported.'); return; } this.previewProfileObj = previewProfile; // 创建previewOutput输出对象 this.previewOutput = this.createPreviewOutputFn(this.cameraManager, this.previewProfileObj, surfaceId); if (this.previewOutput === undefined) { Logger.error(TAG, 'Failed to create the preview stream.'); return; } // 监听预览事件 this.previewOutputCallBack(this.previewOutput); if (this.curSceneMode === camera.SceneMode.NORMAL_PHOTO) { let photoProfile = this.getPhotoProfile(cameraOutputCapability); if (photoProfile === undefined) { Logger.error(TAG, 'The resolution of the current photo stream is not supported.'); return; } this.photoProfileObj = photoProfile; // 创建photoOutPut输出对象 this.photoOutput = this.createPhotoOutputFn(this.cameraManager, this.photoProfileObj); if (this.photoOutput === undefined) { Logger.error(TAG, 'Failed to create the photo stream.'); return; } } else if (this.curSceneMode === camera.SceneMode.NORMAL_VIDEO) { let videoProfile = this.getVideoProfile(cameraOutputCapability); if (videoProfile === undefined) { Logger.error(TAG, 'The resolution of the current video stream is not supported.'); return; } this.videoProfileObj = videoProfile; this.avRecorder = await this.createAVRecorder(); if (this.avRecorder === undefined) { Logger.error(TAG, 'Failed to create the avRecorder.'); return; } await this.prepareAVRecorder(); let videoSurfaceId = await this.avRecorder.getInputSurface(); // 创建videoOutPut输出对象 this.videoOutput = this.createVideoOutputFn(this.cameraManager, this.videoProfileObj, videoSurfaceId); if (this.videoOutput === undefined) { Logger.error(TAG, 'Failed to create the video stream.'); return; } } // 创建cameraInput输出对象 this.cameraInput = this.createCameraInputFn(this.cameraManager, this.curCameraDevice); if (this.cameraInput === undefined) { Logger.error(TAG, 'Failed to create the camera input.'); return; } // 打开相机 let isOpenSuccess = await this.cameraInputOpenFn(this.cameraInput); if (!isOpenSuccess) { Logger.error(TAG, 'Failed to open the camera.'); return; } // 镜头状态回调 this.onCameraStatusChange(this.cameraManager); // 监听CameraInput的错误事件 this.onCameraInputChange(this.cameraInput, this.curCameraDevice); // 会话流程 await this.sessionFlowFn(this.cameraManager, this.cameraInput, this.previewOutput, this.photoOutput, this.videoOutput); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `initCamera fail: ${JSON.stringify(err)}`); } } /** * 获取可变焦距范围 */ getZoomRatioRange(): Array<number> { let zoomRatioRange: Array<number> = []; if (this.session !== undefined) { zoomRatioRange = this.session.getZoomRatioRange(); } return zoomRatioRange; } /** * 变焦 */ setZoomRatioFn(zoomRatio: number): void { Logger.info(TAG, `setZoomRatioFn value ${zoomRatio}`); // 获取支持的变焦范围 try { let zoomRatioRange = this.getZoomRatioRange(); Logger.info(TAG, `getZoomRatioRange success: ${JSON.stringify(zoomRatioRange)}`); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `getZoomRatioRange fail: ${JSON.stringify(err)}`); } try { this.session?.setZoomRatio(zoomRatio); Logger.info(TAG, 'setZoomRatioFn success'); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `setZoomRatioFn fail: ${JSON.stringify(err)}`); } } /** * 以指定参数触发一次拍照 */ async takePicture(): Promise<void> { Logger.info(TAG, 'takePicture start'); let cameraDeviceIndex = GlobalContext.get().getT<number>('cameraDeviceIndex'); let photoSettings: camera.PhotoCaptureSetting = { quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, mirror: cameraDeviceIndex ? true : false }; await this.photoOutput?.capture(photoSettings); Logger.info(TAG, 'takePicture end'); } /** * 释放会话及其相关参数 */ async releaseCamera(): Promise<void> { Logger.info(TAG, 'releaseCamera is called'); try { await this.previewOutput?.release(); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `previewOutput release fail: error: ${JSON.stringify(err)}`); } finally { this.previewOutput = undefined; } try { await this.photoOutput?.release(); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `photoOutput release fail: error: ${JSON.stringify(err)}`); } finally { this.photoOutput = undefined; } try { await this.avRecorder?.release(); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `avRecorder release fail: error: ${JSON.stringify(err)}`); } finally { this.avRecorder = undefined; } try { await this.videoOutput?.release(); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `videoOutput release fail: error: ${JSON.stringify(err)}`); } finally { this.videoOutput = undefined; } try { await this.session?.release(); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `captureSession release fail: error: ${JSON.stringify(err)}`); } finally { this.session = undefined; } try { await this.cameraInput?.close(); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `cameraInput close fail: error: ${JSON.stringify(err)}`); } finally { this.cameraInput = undefined; } this.offCameraStatusChange(); Logger.info(TAG, 'releaseCamera success'); } /** * 获取相机管理器实例 */ getCameraManagerFn(): camera.CameraManager | undefined { if (this.cameraManager) { return this.cameraManager; } let cameraManager: camera.CameraManager | undefined = undefined; try { cameraManager = camera.getCameraManager(GlobalContext.get().getCameraSettingContext()); Logger.info(TAG, `getCameraManager success: ${cameraManager}`); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `getCameraManager failed: ${JSON.stringify(err)}`); } return cameraManager; } /** * 获取支持指定的相机设备对象 */ getSupportedCamerasFn(cameraManager: camera.CameraManager): Array<camera.CameraDevice> { let supportedCameras: Array<camera.CameraDevice> = []; try { supportedCameras = cameraManager.getSupportedCameras(); Logger.info(TAG, `getSupportedCameras success: ${this.cameras}, length: ${this.cameras?.length}`); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `getSupportedCameras failed: ${JSON.stringify(err)}`); } return supportedCameras; } /** * 创建previewOutput输出对象 */ createPreviewOutputFn(cameraManager: camera.CameraManager, previewProfileObj: camera.Profile, surfaceId: string): camera.PreviewOutput | undefined { let previewOutput: camera.PreviewOutput | undefined = undefined; try { previewOutput = cameraManager.createPreviewOutput(previewProfileObj, surfaceId); Logger.info(TAG, `createPreviewOutput success: ${previewOutput}`); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `createPreviewOutput failed: ${JSON.stringify(err)}`); } return previewOutput; } /** * 创建photoOutPut输出对象 */ createPhotoOutputFn(cameraManager: camera.CameraManager, photoProfileObj: camera.Profile): camera.PhotoOutput | undefined { let photoOutput: camera.PhotoOutput | undefined = undefined; try { photoOutput = cameraManager.createPhotoOutput(photoProfileObj); Logger.info(TAG, `createPhotoOutputFn success: ${photoOutput}`); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `createPhotoOutputFn failed: ${JSON.stringify(err)}`); } return photoOutput; } /** * 创建videoOutPut输出对象 */ createVideoOutputFn(cameraManager: camera.CameraManager, videoProfileObj: camera.VideoProfile, surfaceId: string): camera.VideoOutput | undefined { let videoOutput: camera.VideoOutput | undefined = undefined; try { videoOutput = cameraManager.createVideoOutput(videoProfileObj, surfaceId); Logger.info(TAG, `createVideoOutputFn success: ${videoOutput}`); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `createVideoOutputFn failed: ${JSON.stringify(err)}`); } return videoOutput; } /** * 创建cameraInput输出对象 */ createCameraInputFn(cameraManager: camera.CameraManager, cameraDevice: camera.CameraDevice): camera.CameraInput | undefined { Logger.info(TAG, 'createCameraInputFn is called.'); let cameraInput: camera.CameraInput | undefined = undefined; try { cameraInput = cameraManager.createCameraInput(cameraDevice); Logger.info(TAG, 'createCameraInputFn success'); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `createCameraInputFn failed: ${JSON.stringify(err)}`); } return cameraInput; } /** * 打开相机 */ async cameraInputOpenFn(cameraInput: camera.CameraInput): Promise<boolean> { let isOpenSuccess = false; try { await cameraInput.open(); isOpenSuccess = true; Logger.info(TAG, 'cameraInput open success'); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `createCameraInput failed : ${JSON.stringify(err)}`); } return isOpenSuccess; } /** * 会话流程 */ async sessionFlowFn(cameraManager: camera.CameraManager, cameraInput: camera.CameraInput, previewOutput: camera.PreviewOutput, photoOutput: camera.PhotoOutput | undefined, videoOutput: camera.VideoOutput | undefined): Promise<void> { try { // 创建CaptureSession实例 if (this.curSceneMode === camera.SceneMode.NORMAL_PHOTO) { this.session = cameraManager.createSession(this.curSceneMode) as camera.PhotoSession; } else if (this.curSceneMode === camera.SceneMode.NORMAL_VIDEO) { this.session = cameraManager.createSession(this.curSceneMode) as camera.VideoSession; } if (this.session === undefined) { return; } this.onSessionErrorChange(this.session); // 开始配置会话 this.session.beginConfig(); // 把CameraInput加入到会话 this.session.addInput(cameraInput); // 把previewOutput加入到会话 this.session.addOutput(previewOutput); if (this.curSceneMode === camera.SceneMode.NORMAL_PHOTO) { if (photoOutput === undefined) { return; } // 拍照监听事件 this.photoOutputCallBack(photoOutput); // 把photoOutPut加入到会话 this.session.addOutput(photoOutput); } else if (this.curSceneMode === camera.SceneMode.NORMAL_VIDEO) { if (videoOutput === undefined) { return; } // 把photoOutPut加入到会话 this.session.addOutput(videoOutput); } // 提交配置信息 await this.session.commitConfig(); if (this.curSceneMode === camera.SceneMode.NORMAL_VIDEO) { this.setVideoStabilizationFn(this.session as camera.VideoSession, camera.VideoStabilizationMode.MIDDLE); } this.updateSliderValue(); this.setFocusMode(camera.FocusMode.FOCUS_MODE_AUTO); // 开始会话工作 await this.session.start(); Logger.info(TAG, 'sessionFlowFn success'); } catch (error) { let err = error as BusinessError; Logger.error(TAG, `sessionFlowFn fail : ${JSON.stringify(err)}`); } } setVideoStabilizationFn(session: camera.VideoSession, videoStabilizationMode: camera.VideoStabilizationMode): void { // 查询是否支持指定的视频防抖模式 let isVideoStabilizationModeSupported: boolean = session.isVideoStabilizationModeSupported(videoStabilizationMode); if (isVideoStabilizationModeSupported) { session.setVideoStabilizationMode(videoStabilizationMode); } Logger.info(TAG, 'setVideoStabilizationFn success'); } /** * 更新滑动条数据 */ updateSliderValue(): void { let zoomRatioRange = this.getZoomRatioRange(); if (zoomRatioRange.length !== 0) { let zoomRatioMin = zoomRatioRange[0]; let zoomRatioMax = zoomRatioRange[1] > Constants.ZOOM_RADIO_MAX ? Constants.ZOOM_RADIO_MAX : zoomRatioRange[1]; let sliderStep = zoomRatioRange[1] > Constants.ZOOM_RADIO_MAX ? Constants.ZOOM_RADIO_MAX_STEP : Constants.ZOOM_RADIO_MIN_STEP; AppStorage.set<SliderValue>('sliderValue', { min: zoomRatioMin, max: zoomRatioMax, step: sliderStep }); } } /** * 监听拍照事件 */ photoOutputCallBack(photoOutput: camera.PhotoOutput): void { try { // 监听拍照开始 photoOutput.on('captureStartWithInfo', (err: BusinessError, captureStartInfo: camera.CaptureStartInfo): void => { Logger.info(TAG, `photoOutputCallBack captureStartWithInfo success: ${JSON.stringify(captureStartInfo)}`); }); // 监听拍照帧输出捕获 photoOutput.on('frameShutter', (err: BusinessError, frameShutterInfo: camera.FrameShutterInfo): void => { Logger.info(TAG, `photoOutputCallBack frameShutter captureId: ${frameShutterInfo.captureId}, timestamp: ${frameShutterInfo.timestamp}`); }); // 监听拍照结束 photoOutput.on('captureEnd', (err: BusinessError, captureEndInfo: camera.CaptureEndInfo): void => { Logger.info(TAG, `photoOutputCallBack captureEnd captureId: ${captureEndInfo.captureId}, frameCount: ${captureEndInfo.frameCount}`); }); // 监听拍照异常 photoOutput.on('error', (data: BusinessError): void => { Logger.info(TAG, `photoOutPut data: ${JSON.stringify(data)}`); }); photoOutput.on('photoAssetAvailable', (err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset) => { Logger.info(TAG, 'photoAssetAvailable begin'); if (photoAsset === undefined) { Logger.error(TAG, 'photoAsset is undefined'); return; } this.handlePhotoAssetCb(photoAsset); }); } catch (err) { Logger.error(TAG, 'photoOutputCallBack error'); } } /** * 监听预览事件 */ previewOutputCallBack(previewOutput: camera.PreviewOutput): void { Logger.info(TAG, 'previewOutputCallBack is called'); try { previewOutput.on('frameStart', (): void => { Logger.debug(TAG, 'Preview frame started'); }); previewOutput.on('frameEnd', (): void => { Logger.debug(TAG, 'Preview frame ended'); }); previewOutput.on('error', (previewOutputError: BusinessError): void => { Logger.info(TAG, `Preview output previewOutputError: ${JSON.stringify(previewOutputError)}`); }); } catch (err) { Logger.error(TAG, 'previewOutputCallBack error'); } } /** * 注册相机状态变化的回调函数 * @param err - 错误信息(如果有) * @param cameraStatusInfo - 相机状态信息 * @returns 无返回值 */ registerCameraStatusChange(err: BusinessError, cameraStatusInfo: camera.CameraStatusInfo): void { Logger.info(TAG, `cameraId: ${cameraStatusInfo.camera.cameraId},status: ${cameraStatusInfo.status}`); } /** * 监听相机状态变化 * @param cameraManager - 相机管理器对象 * @returns 无返回值 */ onCameraStatusChange(cameraManager: camera.CameraManager): void { Logger.info(TAG, 'onCameraStatusChange is called'); try { cameraManager.on('cameraStatus', this.registerCameraStatusChange); } catch (error) { Logger.error(TAG, 'onCameraStatusChange error'); } } /** * 停止监听相机状态变化 * @returns 无返回值 */ offCameraStatusChange(): void { Logger.info(TAG, 'offCameraStatusChange is called'); this.cameraManager?.off('cameraStatus', this.registerCameraStatusChange); } /** * 监听相机输入变化 * @param cameraInput - 相机输入对象 * @param cameraDevice - 相机设备对象 * @returns 无返回值 */ onCameraInputChange(cameraInput: camera.CameraInput, cameraDevice: camera.CameraDevice): void { Logger.info(TAG, `onCameraInputChange is called`); try { cameraInput.on('error', cameraDevice, (cameraInputError: BusinessError): void => { Logger.info(TAG, `onCameraInputChange cameraInput error code: ${cameraInputError.code}`); }); } catch (error) { Logger.error(TAG, 'onCameraInputChange error'); } } /** * 监听捕获会话错误变化 * @param session - 相机捕获会话对象 * @returns 无返回值 */ onSessionErrorChange(session: camera.PhotoSession | camera.VideoSession): void { try { session.on('error', (captureSessionError: BusinessError): void => { Logger.info(TAG, 'onCaptureSessionErrorChange captureSession fail: ' + JSON.stringify(captureSessionError.code)); }); } catch (error) { Logger.error(TAG, 'onCaptureSessionErrorChange error'); } } async createAVRecorder(): Promise<media.AVRecorder | undefined> { let avRecorder: media.AVRecorder | undefined = undefined; try { avRecorder = await media.createAVRecorder(); } catch (error) { Logger.error(TAG, `createAVRecorder error: ${error}`); } return avRecorder; } initFd(): number { Logger.info(TAG, 'initFd is called'); let filesDir = getContext().filesDir; let filePath = filesDir + `/${Date.now()}.mp4`; AppStorage.setOrCreate<string>('filePath', filePath); let file: fileIo.File = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); return file.fd; } async prepareAVRecorder(): Promise<void> { Logger.info(TAG, 'prepareAVRecorder is called'); let fd = this.initFd(); let videoConfig: media.AVRecorderConfig = { audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, profile: { audioBitrate: Constants.AUDIO_BITRATE, audioChannels: Constants.AUDIO_CHANNELS, audioCodec: media.CodecMimeType.AUDIO_AAC, audioSampleRate: Constants.AUDIO_SAMPLE_RATE, fileFormat: media.ContainerFormatType.CFT_MPEG_4, videoBitrate: Constants.VIDEO_BITRATE, videoCodec: media.CodecMimeType.VIDEO_AVC, videoFrameWidth: this.videoProfileObj.size.width, videoFrameHeight: this.videoProfileObj.size.height, videoFrameRate: this.videoProfileObj.frameRateRange.max }, url: `fd://${fd.toString()}`, rotation: this.curCameraDevice?.cameraOrientation }; Logger.info(TAG, `prepareAVRecorder videoConfig: ${JSON.stringify(videoConfig)}`); await this.avRecorder?.prepare(videoConfig).catch((err: BusinessError): void => { Logger.error(TAG, `prepareAVRecorder prepare err: ${JSON.stringify(err)}`); }); } /** * 启动录制 */ async startVideo(): Promise<void> { Logger.info(TAG, 'startVideo is called'); try { await this.videoOutput?.start(); await this.avRecorder?.start(); this.isRecording = true; } catch (error) { let err = error as BusinessError; Logger.error(TAG, `startVideo err: ${JSON.stringify(err)}`); } Logger.info(TAG, 'startVideo End of call'); } /** * 停止录制 */ async stopVideo(): Promise<void> { Logger.info(TAG, 'stopVideo is called'); if (!this.isRecording) { Logger.info(TAG, 'not in recording'); return; } try { if (this.avRecorder) { await this.avRecorder.stop(); } if (this.videoOutput) { await this.videoOutput.stop(); } this.isRecording = false; } catch (error) { let err = error as BusinessError; Logger.error(TAG, `stopVideo err: ${JSON.stringify(err)}`); } Logger.info(TAG, 'stopVideo End of call'); } /** * 闪关灯 */ hasFlashFn(flashMode: camera.FlashMode): void { // 检测是否有闪关灯 let hasFlash = this.session?.hasFlash(); Logger.debug(TAG, `hasFlash success, hasFlash: ${hasFlash}`); // 检测闪光灯模式是否支持 let isFlashModeSupported = this.session?.isFlashModeSupported(flashMode); Logger.debug(TAG, `isFlashModeSupported success, isFlashModeSupported: ${isFlashModeSupported}`); // 设置闪光灯模式 this.session?.setFlashMode(flashMode); } /** * 焦点 */ setFocusPoint(point: camera.Point): void { // 设置焦点 this.session?.setFocusPoint(point); Logger.info(TAG, `setFocusPoint success point: ${JSON.stringify(point)}`); // 获取当前的焦点 let nowPoint: camera.Point | undefined = undefined; nowPoint = this.session?.getFocusPoint(); Logger.info(TAG, `getFocusPoint success, nowPoint: ${JSON.stringify(nowPoint)}`); } /** * 曝光区域 */ isMeteringPoint(point: camera.Point): void { // 获取当前曝光模式 let exposureMode: camera.ExposureMode | undefined = undefined; exposureMode = this.session?.getExposureMode(); Logger.info(TAG, `getExposureMode success, exposureMode: ${exposureMode}`); this.session?.setMeteringPoint(point); let exposurePoint: camera.Point | undefined = undefined; exposurePoint = this.session?.getMeteringPoint(); Logger.info(TAG, `getMeteringPoint exposurePoint: ${JSON.stringify(exposurePoint)}`); } /** * 曝光补偿 */ isExposureBiasRange(exposureBias: number): void { Logger.debug(TAG, `setExposureBias value ${exposureBias}`); // 查询曝光补偿范围 let biasRangeArray: Array<number> | undefined = []; biasRangeArray = this.session?.getExposureBiasRange(); Logger.debug(TAG, `getExposureBiasRange success, biasRangeArray: ${JSON.stringify(biasRangeArray)}`); // 设置曝光补偿 this.session?.setExposureBias(exposureBias); } /** * 对焦模式 */ setFocusMode(focusMode: camera.FocusMode): void { // 检测对焦模式是否支持 Logger.info(TAG, `setFocusMode is called`); let isSupported = this.session?.isFocusModeSupported(focusMode); Logger.info(TAG, `setFocusMode isSupported: ${isSupported}`); // 设置对焦模式 if (!isSupported) { return; } this.session?.setFocusMode(focusMode); } } export default new CameraService();
在CameraService的initCamera函数里完成一个相机生命周期初始化的过程,包括调用getCameraMananger获取CameraMananger,调用getSupportedCameras获取支持的camera设备,调用getSupportedOutputCapability获取支持的camera设备能力集,调用createPreviewOutput创建预览输出,调用createCameraInput创建相机输入,调用CameraInput的open打开相机输入,调用onCameraStatusChange创建CameraManager注册回调,最后调用sessionFlowFn创建并开启Session。
其中sessionFlowFn是一个创建session并开启预览的动作,主要流程包括:调用createSession创建Session,调用beginConfig开始配置会话,调用addInput把CameraInput加入到会话,调用addPreviewOutput把previewOutput加入到会话,调用commitConfig提交配置信息,调用start开始会话工作。
在CameraService的releaseCamera函数里完成对相机生命周期释放的过程,调用output的release方法释放流,调用CameraInput的close方法关闭相机,再调用session的release释放当前会话。
回调接口设置:
onCameraStatusChange:监听相机状态回调,在打开、退出相机,相机摄像头切换时会触发
onCameraInputChange:相机输入发生错误时触发回调
photoOutputCallBack:开启拍照时触发回调
previewOutputCallBack:开启预览时触发回调
onCaptureSessionErrorChange:session出现异常时触发回调
相机预览、拍照,录像功能实现调用侧位于Index.ets,ModeComponent.ets中,
源码参考:[Index.ets]
/* * Copyright (c) 2024 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { camera } from '@kit.CameraKit'; import CameraService from '../mode/CameraService'; import Logger from '../common/utils/Logger'; import { ModeComponent } from '../views/ModeComponent'; import { SlideComponent } from '../views/SlideComponent'; import { GlobalContext } from '../common/utils/GlobalContext'; import { Constants } from '../common/Constants'; import { FocusAreaComponent } from '../views/FocusAreaComponent'; import { FocusComponent } from '../views/FocusComponent'; import { FlashingLightComponent } from '../views/FlashingLightComponent'; const TAG = 'Index'; @Entry @Component struct Index { // 主页面是否显示 @StorageLink('isShow') isShow: boolean = false; @StorageLink('isOpenEditPage') isOpenEditPage: boolean = false; // Flash Mode @State flashMode: camera.FlashMode = camera.FlashMode.FLASH_MODE_CLOSE; @State focusPointBol: boolean = false; // 曝光区域手指点击坐标 @State focusPointVal: Array<number> = [0, 0]; @State xComponentAspectRatio: number = 1; private mXComponentController: XComponentController = new XComponentController(); private defaultCameraDeviceIndex = 0; private surfaceId = ''; aboutToAppear(): void { Logger.info(TAG, 'aboutToAppear'); } async aboutToDisAppear(): Promise<void> { Logger.info(TAG, 'aboutToDisAppear'); this.flashMode = camera.FlashMode.FLASH_MODE_CLOSE; await CameraService.releaseCamera(); } async onPageShow(): Promise<void> { Logger.info(TAG, 'onPageShow'); if (this.surfaceId !== '' && !this.isOpenEditPage) { await CameraService.initCamera(this.surfaceId, GlobalContext.get().getT<number>('cameraDeviceIndex')); } this.isOpenEditPage = false; } async onPageHide(): Promise<void> { Logger.info(TAG, 'onPageHide'); } build() { Stack() { if (this.isShow) { XComponent({ id: 'componentId', type: 'surface', controller: this.mXComponentController }) .onLoad(async () => { Logger.info(TAG, 'onLoad is called'); this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); GlobalContext.get().setObject('cameraDeviceIndex', this.defaultCameraDeviceIndex); GlobalContext.get().setObject('xComponentSurfaceId', this.surfaceId); let surfaceRect: SurfaceRect = { surfaceWidth: Constants.X_COMPONENT_SURFACE_HEIGHT, surfaceHeight: Constants.X_COMPONENT_SURFACE_WIDTH }; this.mXComponentController.setXComponentSurfaceRect(surfaceRect); Logger.info(TAG, `onLoad surfaceId: ${this.surfaceId}`); await CameraService.initCamera(this.surfaceId, this.defaultCameraDeviceIndex); }) .border({ width: { top: Constants.X_COMPONENT_BORDER_WIDTH, bottom: Constants.X_COMPONENT_BORDER_WIDTH }, color: Color.Black }) // The width and height of the surface are opposite to those of the Xcomponent. .width(px2vp(Constants.X_COMPONENT_SURFACE_HEIGHT)) .height(px2vp(Constants.X_COMPONENT_SURFACE_WIDTH)) } // 曝光框和对焦框 FocusComponent({ focusPointBol: $focusPointBol, focusPointVal: $focusPointVal }) // 曝光对焦手指点击区域 FocusAreaComponent({ focusPointBol: $focusPointBol, focusPointVal: $focusPointVal, xComponentWidth: px2vp(Constants.X_COMPONENT_SURFACE_HEIGHT), xComponentHeight: px2vp(Constants.X_COMPONENT_SURFACE_WIDTH) }) // slide SlideComponent() // 拍照 ModeComponent() Row({ space: Constants.ROW_SPACE_24 }) { // 闪光灯 FlashingLightComponent({ flashMode: $flashMode }) } .margin({ left: Constants.CAPTURE_BUTTON_COLUMN_MARGIN }) .alignItems(VerticalAlign.Top) .justifyContent(FlexAlign.Start) .position({ x: Constants.FLASH_POSITION_X, y: Constants.FLASH_POSITION_Y }) } .size({ width: Constants.FULL_PERCENT, height: Constants.FULL_PERCENT }) .backgroundColor(Color.Black) } }
/* * Copyright (c) 2024 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { router } from '@kit.ArkUI'; import { camera } from '@kit.CameraKit'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; import CameraService from '../mode/CameraService'; import Logger from '../common/utils/Logger'; import { GlobalContext } from '../common/utils/GlobalContext'; import { Constants } from '../common/Constants'; const TAG: string = 'ModeComponent'; @Component export struct ModeComponent { @StorageLink('isOpenEditPage') @Watch('changePageState') isOpenEditPage: boolean = false; @State sceneMode: camera.SceneMode = camera.SceneMode.NORMAL_PHOTO; @State isRecording: boolean = false; changePageState(): void { if (this.isOpenEditPage) { this.onJumpClick(); } } aboutToAppear(): void { Logger.info(TAG, 'aboutToAppear'); CameraService.setSavePictureCallback(this.handleSavePicture); } handleSavePicture = (photoAsset: photoAccessHelper.PhotoAsset): void => { Logger.info(TAG, 'handleSavePicture'); this.setImageInfo(photoAsset); AppStorage.set<boolean>('isOpenEditPage', true); Logger.info(TAG, 'setImageInfo end'); } setImageInfo(photoAsset: photoAccessHelper.PhotoAsset): void { Logger.info(TAG, 'setImageInfo'); GlobalContext.get().setObject('photoAsset', photoAsset); } onJumpClick(): void { GlobalContext.get().setObject('sceneMode', this.sceneMode); // 目标url router.pushUrl({ url: 'pages/EditPage' }, router.RouterMode.Single, (err) => { if (err) { Logger.error(TAG, `Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); return; } Logger.info(TAG, 'Invoke pushUrl succeeded.'); }); } build() { Column() { Row({ space: Constants.COLUMN_SPACE_24 }) { Column() { Text('拍照') .fontSize(Constants.FONT_SIZE_14) .fontColor(Color.White) } .width(Constants.CAPTURE_COLUMN_WIDTH) .backgroundColor(this.sceneMode === camera.SceneMode.NORMAL_PHOTO ? $r('app.color.theme_color') : '') .borderRadius(Constants.BORDER_RADIUS_14) .onClick(async () => { if (this.sceneMode === camera.SceneMode.NORMAL_PHOTO) { return; } this.sceneMode = camera.SceneMode.NORMAL_PHOTO; CameraService.setSceneMode(this.sceneMode); let cameraDeviceIndex = GlobalContext.get().getT<number>('cameraDeviceIndex'); let surfaceId = GlobalContext.get().getT<string>('xComponentSurfaceId'); await CameraService.initCamera(surfaceId, cameraDeviceIndex); }) // 录像 Column() { Text('录像') .fontSize(Constants.FONT_SIZE_14) .fontColor(Color.White) } .width(Constants.CAPTURE_COLUMN_WIDTH) .backgroundColor(this.sceneMode === camera.SceneMode.NORMAL_VIDEO ? $r('app.color.theme_color') : '') .borderRadius(Constants.BORDER_RADIUS_14) .onClick(async () => { if (this.sceneMode === camera.SceneMode.NORMAL_VIDEO) { return; } this.sceneMode = camera.SceneMode.NORMAL_VIDEO; CameraService.setSceneMode(this.sceneMode); let cameraDeviceIndex = GlobalContext.get().getT<number>('cameraDeviceIndex'); let surfaceId = GlobalContext.get().getT<string>('xComponentSurfaceId'); await CameraService.initCamera(surfaceId, cameraDeviceIndex); }) } .height(Constants.CAPTURE_ROW_HEIGHT) .width(Constants.FULL_PERCENT) .justifyContent(FlexAlign.Center) .alignItems(VerticalAlign.Center) Row() { Column() { } .width($r('app.string.200px')) // 拍照-录像 按键 Column() { if (!this.isRecording) { Row() { Button() { Text() .width($r('app.string.120px')) .height($r('app.string.120px')) .borderRadius($r('app.string.40px')) .backgroundColor(this.sceneMode === camera.SceneMode.NORMAL_VIDEO ? $r('app.color.theme_color') : Color.White) } .border({ width: Constants.CAPTURE_BUTTON_BORDER_WIDTH, color: $r('app.color.border_color'), radius: Constants.CAPTURE_BUTTON_BORDER_RADIUS }) .width($r('app.string.200px')) .height($r('app.string.200px')) .backgroundColor(Color.Black) .onClick(async () => { if (this.sceneMode === camera.SceneMode.NORMAL_PHOTO) { await CameraService.takePicture(); } else { await CameraService.startVideo(); this.isRecording = true; } }) } } else { Row() { // 录像停止键 Button() { Image($r('app.media.ic_camera_video_close')) .size({ width: Constants.IMAGE_SIZE, height: Constants.IMAGE_SIZE }) } .width($r('app.string.120px')) .height($r('app.string.120px')) .backgroundColor($r('app.color.theme_color')) .onClick(() => { this.isRecording = !this.isRecording; CameraService.stopVideo().then(() => { this.isOpenEditPage = true; Logger.info(TAG, 'stopVideo success'); }) }) } .width($r('app.string.200px')) .height($r('app.string.200px')) .borderRadius($r('app.string.60px')) .backgroundColor($r('app.color.theme_color')) .justifyContent(FlexAlign.SpaceAround) } } // 前后置摄像头切换 Column() { Row() { Button() { Image($r('app.media.switch_camera')) .width($r('app.string.120px')) .height($r('app.string.120px')) } .width($r('app.string.200px')) .height($r('app.string.200px')) .backgroundColor($r('app.color.flash_background_color')) .borderRadius($r('app.string.40px')) .onClick(async () => { let cameraDeviceIndex = GlobalContext.get().getT<number>('cameraDeviceIndex'); let surfaceId = GlobalContext.get().getT<string>('xComponentSurfaceId'); cameraDeviceIndex ? cameraDeviceIndex = 0 : cameraDeviceIndex = 1; GlobalContext.get().setObject('cameraDeviceIndex', cameraDeviceIndex); await CameraService.initCamera(surfaceId, cameraDeviceIndex); }) } } .visibility(this.isRecording ? Visibility.Hidden : Visibility.Visible) } .padding({ left: Constants.CAPTURE_BUTTON_COLUMN_PADDING, right: Constants.CAPTURE_BUTTON_COLUMN_PADDING }) .width(Constants.FULL_PERCENT) .justifyContent(FlexAlign.SpaceBetween) .alignItems(VerticalAlign.Center) } .justifyContent(FlexAlign.End) .height(Constants.TEN_PERCENT) .padding({ left: Constants.CAPTURE_BUTTON_COLUMN_PADDING, right: Constants.CAPTURE_BUTTON_COLUMN_PADDING }) .margin({ bottom: Constants.CAPTURE_BUTTON_COLUMN_MARGIN }) .position({ x: Constants.ZERO_PERCENT, y: Constants.EIGHTY_FIVE_PERCENT }) } }
预览:开启预览位于Index.ets下的XComponent组件的onLoad接口中,其中调用CameraService.initCamera方法,将预览的surfaceId,摄像头设备作为入参啊传下去,完成开启相机的操作,开启预览。
拍照:开启拍照位于ModeComponent.ets下的拍照按钮的onClick接口,调用CameraManager对象下的takePicture方法开启拍照操作。
相机变焦功能实现调用侧位于SlideComponent.ets中,源码参考:[SlideComponent.ets]
/* * Copyright (c) 2023 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import CameraService, { SliderValue } from '../mode/CameraService'; import Logger from '../common/utils/Logger'; import { Constants } from '../common/Constants'; const TAG: string = 'SlideComponent'; // 变焦组件 @Component export struct SlideComponent { // slide滑块 @StorageLink('zoomRatio') zoomRatio: number = 1; @StorageLink('sliderValue') sliderValue: SliderValue | undefined = undefined; private fractionDigits = 2; private xString = 'x'; aboutToDisappear(): void { } slideChange(value: number): void { CameraService.setZoomRatioFn(value); } build() { if (this.sliderValue) { Column() { Row() { Text(this.zoomRatio + this.xString) .fontColor($r('app.color.slide_text_font_color')) .width($r('app.string.120px')) .height($r('app.string.50px')) .borderRadius(Constants.TEXT_BORDER_RADIUS) .backgroundColor(Color.White) .fontSize(Constants.FONT_SIZE_14) .textAlign(TextAlign.Center) } .justifyContent(FlexAlign.Center) .width(Constants.FULL_PERCENT) Row() { Text(this.sliderValue?.min + this.xString).fontColor(Color.White) Text(this.sliderValue?.max + this.xString).fontColor(Color.White) } .justifyContent(FlexAlign.SpaceBetween).width(Constants.FULL_PERCENT) Row() { Slider({ value: this.zoomRatio, min: this.sliderValue?.min, max: this.sliderValue?.max, step: this.sliderValue?.step, style: SliderStyle.OutSet }) .showSteps(false) .trackColor($r('app.color.slider_track_color')) .selectedColor($r('app.color.theme_color')) .onChange((value: number) => { Logger.info(TAG, 'onChange'); let val = Number(value.toFixed(this.fractionDigits)); this.slideChange(val); this.zoomRatio = val; }) } .width(Constants.FULL_PERCENT) } .height($r('app.string.60px')) .width(Constants.FORTY_PERCENT) .position({ x: Constants.THIRTY_PERCENT, y: Constants.SEVENTY_FIVE_PERCENT }) } } }
变焦:变焦功能位于SlideComponent.ets,通过Slider组件的onChange接口将变焦率通过CameraService.setZoomRatioFn方法调整预览显示的画面。
相机对焦功能实现调用侧位于FocusAreaComponent.ets,FocusComponent.ets中,
源码参考:[FocusAreaComponent.ets]
/* * Copyright (c) 2024 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Logger from '../common/utils/Logger'; import CameraService from '../mode/CameraService'; const TAG: string = 'FocusAreaComponent'; // 对焦区域 @Component export struct FocusAreaComponent { @Link focusPointBol: boolean; @Link focusPointVal: Array<number>; @Prop xComponentWidth: number; @Prop xComponentHeight: number; // 对焦区域显示框定时器 private areaTimer: number = -1; private focusFrameDisplayDuration: number = 3500; build() { Row() { } .width(this.xComponentWidth) .height(this.xComponentHeight) .opacity(1) .onTouch((e: TouchEvent) => { if (e.type === TouchType.Down) { this.focusPointBol = true; this.focusPointVal[0] = e.touches[0].windowX; this.focusPointVal[1] = e.touches[0].windowY; // 归一化焦点。 设置的焦点与相机sensor角度和窗口方向有关(相机sensor角度可通过CameraDevice的cameraOrientation属性获取),下面焦点是以竖屏窗口,相机sensor角度为90度场景下的焦点设置 CameraService.setFocusPoint({ x: e.touches[0].y / this.xComponentHeight, y: 1 - (e.touches[0].x / this.xComponentWidth) }); } if (e.type === TouchType.Up) { if (this.areaTimer) { clearTimeout(this.areaTimer); } this.areaTimer = setTimeout(() => { this.focusPointBol = false; }, this.focusFrameDisplayDuration); } }) .onClick((event: ClickEvent) => { Logger.info(TAG, 'onClick is called'); }) } }
/* * Copyright (c) 2024 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // 曝光选择 @Component export struct FocusComponent { @Link focusPointBol: boolean; @Link focusPointVal: Array<number>; private mBorderWidth = 1.6; private mBorderRadius = 10; private mRowSize = 40; private mFocusPoint = 60; private focusFrameSize = 120; build() { if (this.focusPointBol) { Row() { // 对焦框 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween }) { Flex({ justifyContent: FlexAlign.SpaceBetween }) { Row() { } .border({ width: { left: this.mBorderWidth, top: this.mBorderWidth }, color: Color.White, radius: { topLeft: this.mBorderRadius } }) .size({ width: this.mRowSize, height: this.mRowSize }) Row() { } .border({ width: { right: this.mBorderWidth, top: this.mBorderWidth }, color: Color.White, radius: { topRight: this.mBorderRadius } }) .size({ width: this.mRowSize, height: this.mRowSize }) } Flex({ justifyContent: FlexAlign.SpaceBetween }) { Row() { } .border({ width: { left: this.mBorderWidth, bottom: this.mBorderWidth }, color: Color.White, radius: { bottomLeft: this.mBorderRadius } }) .size({ width: this.mRowSize, height: this.mRowSize }) Row() { } .border({ width: { right: this.mBorderWidth, bottom: this.mBorderWidth }, color: Color.White, radius: { bottomRight: this.mBorderRadius } }) .size({ width: this.mRowSize, height: this.mRowSize }) } } .width(this.focusFrameSize) .height(this.focusFrameSize) .position({ x: this.focusPointVal[0] - this.mFocusPoint, y: this.focusPointVal[1] - this.mFocusPoint }) } .zIndex(1) } } }
除此之外,根据这个学习鸿蒙全栈学习路线,也附带一整套完整的学习【文档+视频】,内容包含如下:
内容包含了:(ArkTS、ArkUI、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战)等技术知识点。帮助大家在学习鸿蒙路上快速成长!
为了避免大家在学习过程中产生更多的时间成本,对比我把以上内容全部放在了↓↓↓想要的可以自拿喔!谢谢大家观看!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。