• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  ![Camera Distributed processing](figures/camera-distributed-process.png)
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”。