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