1# 分布式相机开发指南 2<!--Kit: Distributed Service Kit--> 3<!--Subsystem: DistributedHardware--> 4<!--Owner: @hobbycao--> 5<!--Designer: @saga_2025--> 6<!--Tester: @wei-guoqing1--> 7<!--Adviser: @w_Machine_cc--> 8 9## 简介 10 11 OpenHarmony分布式相机通过打破硬件边界,实现了跨设备的摄像头能力协同。当搭载OpenHarmony系统的设备A与设备B完成组网后,设备A的应用可实时调用设备B的摄像头资源,获取对方影像(预览流/拍照流/录像流),且支持分辨率调节、参数同步等深度控制。这一功能在以下场景中具有突破性应用价值,例如: 12 - 多视角协同创作 13 - 远程专家协作 14 - 沉浸式安防系统 15 - 分布式影音交互 16 17 18### 基本概念 19 20 在进行分布式相机开发前,建议开发者查看下列章节,了解相关功能操作: 21 - [应用跨设备连接](abilityconnectmanager-guidelines.md) 22 - [相机管理](../media/camera/camera-device-management.md) 23 - [申请相关权限](../media/camera/camera-preparation.md) 24 - [会话管理](../media/camera/camera-session-management.md) 25 - [拍照](../media/camera/camera-shooting.md) 26 - [录像](../media/camera/camera-recording.md) 27 28 29## 环境准备 30 31### 环境要求 32 33 设备A和设备B之间需要组网成功,并通过分布式硬件管理框架上线设备。 34 35 36### 搭建环境 37 38 1. 安装[DevEco Studio](https://developer.huawei.com/consumer/cn/download/deveco-studio),要求版本在5.0及以上。 39 2. 将public-SDK更新到API 16或以上<!--Del-->,更新SDK的具体操作可参见[更新指南](../tools/openharmony_sdk_upgrade_assistant.md)<!--DelEnd-->。 40 3. 用USB线缆将两台调测设备(设备A和设备B)连接到PC。 41 4. 打开设备A和设备B的Wifi并连接到同一个接入点上,互相识别,连接并组网。连接组网的具体操作可参见[创建会话并连接](abilityconnectmanager-guidelines.md#开发步骤)。 42 43 44### 检验环境是否搭建成功 45 46 PC上执行shell命令: 47 48 ```shell 49 hdc shell 50 hidumper -s 4700 -a "buscenter -l remote_device_info" 51 ``` 52 53 组网成功时可显示组网设备数量的信息,如“remote device num = 1”。 54 55 56## 开发指导 57 58 通过OpenHarmony操作系统,将用户拥有的多个设备相机资源作为一个硬件池,为用户提供跨端使用相机的能力。 59 60### 开发流程 61 62 分布式相机流程图建议如下: 63 64  65 66 67### 开发步骤 68 69**导入相机和多媒体等模块文件** 70 71 ```ts 72 import { camera } from '@kit.CameraKit'; 73 import { media } from '@kit.MediaKit'; 74 ``` 75 76**赋予应用访问权限** 77 78 应用需申请权限,包括但不限于下列权限类型: 79 - 图片和视频 ohos.permission.MEDIA_LOCATION 80 - 文件读 ohos.permission.READ_MEDIA 81 - 文件写 ohos.permission.WRITE_MEDIA 82 - 相机 ohos.permission.CAMERA 83 - 多设备协同 ohos.permission.DISTRIBUTED_DATASYNC 84 85 例如在UIAbility申请相关的访问权限,通过调用requestPermissionsFromUser()方法添加对应的权限类型。 86 ```ts 87 //EntryAbility.ets 88 export default class EntryAbility extends UIAbility { 89 onCreate(want, launchParam) { 90 Logger.info('Sample_VideoRecorder', 'Ability onCreate,requestPermissionsFromUser'); 91 let permissionNames: Array<Permissions> = ['ohos.permission.MEDIA_LOCATION', 'ohos.permission.READ_MEDIA', 92 'ohos.permission.WRITE_MEDIA', 'ohos.permission.CAMERA', 'ohos.permission.MICROPHONE', 'ohos.permission.DISTRIBUTED_DATASYNC']; 93 abilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context, permissionNames).then((data)=> { 94 console.log("testTag", data); 95 }) 96 .catch((err : BusinessError) => { 97 console.error("testTag", err.message); 98 }); 99 } 100 ``` 101 102 103**启动分布式相机预览流及拍照流** 104 105**1. 获取远端设备相机信息** 106 107 应用组网成功后,需获取远端设备信息,通过getCameraManager()方法获取相机管理器实例,getSupportedCameras()方法获取支持指定的相机设备对象。 108 109 ```ts 110 private cameras?: Array<camera.CameraDevice>; 111 private cameraManager?: camera.CameraManager; 112 private cameraOutputCapability?: camera.CameraOutputCapability; 113 private cameraIndex: number = 0; 114 private curVideoProfiles?: Array<camera.VideoProfile>; 115 116 function initCamera(): void { 117 console.info('init remote camera called'); 118 if (this.cameraManager) { 119 console.info('cameraManager already exits'); 120 return; 121 } 122 console.info('[camera] case to get cameraManager'); 123 this.cameraManager = camera.getCameraManager(globalThis.abilityContext); 124 if (this.cameraManager) { 125 console.info('[camera] case getCameraManager success'); 126 } else { 127 console.error('[camera] case getCameraManager failed'); 128 return; 129 } 130 this.cameras = this.cameraManager.getSupportedCameras(); 131 if (this.cameras) { 132 console.info('[camera] case getCameras success, size ', this.cameras.length); 133 for (let i = 0; i < this.cameras.length; i++) { 134 let came: camera.CameraDevice = this.cameras[i]; 135 console.info('[came] camera json:', JSON.stringify(came)); 136 if (came.connectionType == camera.ConnectionType.CAMERA_CONNECTION_REMOTE) { 137 this.cameraIndex = i; 138 this.cameraOutputCapability = this.cameraManager.getSupportedOutputCapability(came); 139 this.curVideoProfiles = this.cameraOutputCapability.videoProfiles; 140 console.info('init remote camera done'); //初始化远端摄像头成功 141 break; 142 } 143 } 144 } else { 145 console.error('[camera] case getCameras failed'); 146 } 147 } 148 ``` 149 150**2. 创建CameraInput实例** 151 152 获取相机管理器实例和支持指定的相机设备对象后,通过createCameraInput()方法创建CameraInput实例。 153 154 ```ts 155 // create camera input 156 async createCameraInput(): Promise<void> { 157 console.log('createCameraInput called'); 158 if (this.cameras && this.cameras.length > 0) { 159 let came: camera.CameraDevice = this.cameras[this.cameraIndex]; 160 console.log('[came]createCameraInput camera json:', JSON.stringify(came)); 161 this.cameraInput = this.cameraManager?.createCameraInput(came); 162 if (this.cameraInput) { 163 console.log('[camera] case createCameraInput success'); 164 await this.cameraInput.open().then(() => { 165 console.log('[camera] case cameraInput.open() success'); 166 }).catch((err: Error) => { 167 console.error('[camera] cameraInput.open then.error:', JSON.stringify(err)); 168 }); 169 } else { 170 console.error('[camera] case createCameraInput failed'); 171 return; 172 } 173 } 174 } 175 ``` 176 177**3. 获取预览输出对象** 178 179 通过createPreviewOutput()方法创建预览输出对象。 180 181 ```ts 182 private previewOutput?: camera.PreviewOutput; 183 private avConfig: media.AVRecorderConfig = { 184 videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, 185 profile: this.avProfile, 186 url: 'fd://', 187 } 188 189 // create camera preview 190 async createPreviewOutput(): Promise<void> { 191 console.log('createPreviewOutput called'); 192 if (this.cameraOutputCapability && this.cameraManager) { 193 this.previewProfiles = this.cameraOutputCapability.previewProfiles; 194 console.log('[camera] this.previewProfiles json ', JSON.stringify(this.previewProfiles)); 195 if (this.previewProfiles[0].format === camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP) { 196 console.log('[camera] case format is VIDEO_SOURCE_TYPE_SURFACE_YUV'); 197 this.avConfig.videoSourceType = media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV; 198 } else { 199 console.log('[camera] case format is VIDEO_SOURCE_TYPE_SURFACE_ES'); 200 this.avConfig.videoSourceType = media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_ES; 201 } 202 this.previewOutput = this.cameraManager.createPreviewOutput(this.previewProfiles[0], this.surfaceId); 203 if (!this.previewOutput) { 204 console.error('create previewOutput failed!'); 205 } 206 console.log('createPreviewOutput done'); 207 } 208 } 209 ``` 210 211 212**4. 获取拍照输出对象** 213 214 通过createPhotoOutput()方法创建拍照输出对象,通过createImageReceiver()方法创建ImageReceiver实例。 215 216 ```ts 217 import fileio from '@ohos.fileio'; 218 219 private photoReceiver?: image.ImageReceiver; 220 private photoOutput?: camera.PhotoOutput; 221 private mSaveCameraAsset: SaveCameraAsset = new SaveCameraAsset('Sample_VideoRecorder'); 222 223 async getImageFileFd(): Promise<void> { 224 console.info('getImageFileFd called'); 225 this.mFileAssetId = await this.mSaveCameraAsset.createImageFd(); 226 this.fdPath = 'fd://' + this.mFileAssetId.toString(); 227 this.avConfig.url = this.fdPath; 228 console.info('ImageFileFd is: ' + this.fdPath); 229 console.info('getImageFileFd done'); 230 } 231 232 // close file fd 233 async closeFd(): Promise<void> { 234 console.info('case closeFd called'); 235 if (this.mSaveCameraAsset) { 236 await this.mSaveCameraAsset.closeVideoFile(); 237 this.mFileAssetId = undefined; 238 this.fdPath = undefined; 239 console.info('case closeFd done'); 240 } 241 } 242 243 async createPhotoOutput() { 244 const photoProfile: camera.Profile = { 245 format: camera.CameraFormat.CAMERA_FORMAT_JPEG, 246 size: { 247 "width": 1280, 248 "height": 720 249 } 250 } 251 if (!this.cameraManager) { 252 console.error('createPhotoOutput cameraManager is null') 253 } 254 if (!this.photoReceiver) { 255 this.photoReceiver = image.createImageReceiver(photoProfile.size.width, photoProfile.size.height, photoProfile.format, 8) 256 this.photoReceiver.on("imageArrival",()=>{ 257 this.photoReceiver?.readNextImage((err,image)=>{ 258 if (err || image === undefined) { 259 console.error('photoReceiver imageArrival on error') 260 return; 261 } 262 image.getComponent(4, async (err, img) => { 263 if (err || img === undefined) { 264 console.error('image getComponent on error') 265 return; 266 } 267 await this.getImageFileFd() 268 fileio.write(this.mFileAssetId, img.byteBuffer) 269 await this.closeFd() 270 await image.release() 271 console.log('photoReceiver image.getComponent save success') 272 }) 273 }) 274 }) 275 await this.photoReceiver.getReceivingSurfaceId().then((surfaceId: string) => { 276 this.photoOutput = this.cameraManager?.createPhotoOutput(photoProfile, surfaceId) 277 if (!this.photoOutput) { 278 console.error('cameraManager.createPhotoOutput on error') 279 } 280 console.log('cameraManager.createPhotoOutput success') 281 this.photoOutput?.on("captureStart", (err, captureId) => { 282 console.log('photoOutput.on captureStart') 283 }) 284 }).catch((err: Error) => { 285 console.error('photoReceiver.getReceivingSurfaceId on error:' + err) 286 }) 287 } 288 } 289 ``` 290 291**5. 创建CaptureSession实例** 292 293通过createCaptureSession()方法创建CaptureSession实例。调用beginConfig()方法开始配置会话,使用addInput()和addOutput()方法将CameraInput()和CameraOutput()加入到会话,最后调用commitConfig()方法提交配置信息,通过Promise获取结果。 294 295 ```ts 296 private captureSession?: camera.CaptureSession; 297 298 function failureCallback(error: BusinessError): Promise<void> { 299 console.error('case failureCallback called,errMessage is ', JSON.stringify(error)); 300 } 301 302 function catchCallback(error: BusinessError): Promise<void> { 303 console.error('case catchCallback called,errMessage is ', JSON.stringify(error)); 304 } 305 306 // create camera capture session 307 async createCaptureSession(): Promise<void> { 308 console.log('createCaptureSession called'); 309 if (this.cameraManager) { 310 this.captureSession = this.cameraManager.createCaptureSession(); 311 if (!this.captureSession) { 312 console.error('createCaptureSession failed!'); 313 return; 314 } 315 try { 316 this.captureSession.beginConfig(); 317 this.captureSession.addInput(this.cameraInput); 318 } catch (e) { 319 console.error('case addInput error:' + JSON.stringify(e)); 320 } 321 try { 322 this.captureSession.addOutput(this.previewOutput); 323 } catch (e) { 324 console.error('case addOutput error:' + JSON.stringify(e)); 325 } 326 await this.captureSession.commitConfig().then(() => { 327 console.log('captureSession commitConfig success'); 328 }, this.failureCallback).catch(this.catchCallback); 329 } 330 } 331 ``` 332 333**6. 开启会话工作** 334 335 通过CaptureSession实例上的start()方法开始会话工作,通过Promise获取结果。 336 337 ```ts 338 // start captureSession 339 async startCaptureSession(): Promise<void> { 340 console.log('startCaptureSession called'); 341 if (!this.captureSession) { 342 console.error('CaptureSession does not exist!'); 343 return; 344 } 345 346 try { 347 await this.captureSession.start(); 348 console.info('CaptureSession started successfully.'); 349 } catch (error) { 350 console.error('Failed to start CaptureSession:', error); 351 if (this.failureCallback) { 352 this.failureCallback(error); 353 } 354 } 355 } 356 ``` 357 358**释放分布式相机资源** 359 360 业务协同完毕后需及时结束协同状态,释放分布式相机资源。 361 362 ```ts 363 // 释放相机 364 async releaseCameraInput(): Promise<void> { 365 console.log('releaseCameraInput called'); 366 if (this.cameraInput) { 367 this.cameraInput = undefined; 368 } 369 console.log('releaseCameraInput done'); 370 } 371 372 // 释放预览 373 async releasePreviewOutput(): Promise<void> { 374 console.log('releasePreviewOutput called'); 375 if (this.previewOutput) { 376 await this.previewOutput.release().then(() => { 377 console.log('[camera] case main previewOutput release called'); 378 }, this.failureCallback).catch(this.catchCallback); 379 this.previewOutput = undefined; 380 } 381 console.log('releasePreviewOutput done'); 382 } 383 384 // 释放视频输出 385 async releaseVideoOutput(): Promise<void> { 386 console.log('releaseVideoOutput called'); 387 if (this.videoOutput) { 388 await this.videoOutput.release().then(() => { 389 console.log('[camera] case main videoOutput release called'); 390 }, this.failureCallback).catch(this.catchCallback); 391 this.videoOutput = undefined; 392 } 393 console.log('releaseVideoOutput done'); 394 } 395 396 // 停止拍照任务 397 async stopCaptureSession(): Promise<void> { 398 console.log('stopCaptureSession called'); 399 if (this.captureSession) { 400 await this.captureSession.stop().then(() => { 401 console.log('[camera] case main captureSession stop success'); 402 }, this.failureCallback).catch(this.catchCallback); 403 } 404 console.log('stopCaptureSession done'); 405 } 406 407 // 释放拍照任务 408 async releaseCaptureSession(): Promise<void> { 409 console.log('releaseCaptureSession called'); 410 if (this.captureSession) { 411 await this.captureSession.release().then(() => { 412 console.log('[camera] case main captureSession release success'); 413 }, this.failureCallback).catch(this.catchCallback); 414 this.captureSession = undefined; 415 } 416 console.log('releaseCaptureSession done'); 417 } 418 419 // 释放相机资源 420 async releaseCamera(): Promise<void> { 421 console.log('releaseCamera called'); 422 await this.stopCaptureSession(); 423 await this.releaseCameraInput(); 424 await this.releasePreviewOutput(); 425 await this.releaseVideoOutput(); 426 await this.releaseCaptureSession(); 427 console.log('releaseCamera done'); 428 } 429 ``` 430 431### 调测验证 432 433 应用侧开发完成后,可在设备A和设备B上安装应用,测试步骤如下: 434 435 1. 设备A拉起设备B上的分布式摄像头并发起预览,设备A能接收到预览流。 436 2. 设备A拉起设备B上的分布式摄像头并拍照,设备A能接收到照片。 437 438## 常见问题 439 440 441### 设备A应用无法拉起设备B摄像头 442 443**可能原因** 444 445 设备间没有相互组网或者组网后中断了连接。 446 447**解决措施** 448 449 设备A和设备B开启USB调试功能,用USB线连接设备和PC。执行shell命令: 450 451 ```shell 452 hdc shell 453 hidumper -s 4700 -a "buscenter -l remote_device_info" 454 ``` 455 回显信息为 “remote device num = 0” 即为组网失败,请禁用再启用Wifi重新接入到同一个接入点上。组网成功后重新执行命令会显示正确组网设备数量的信息,如“remote device num = 1”。