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