鸿蒙分布式相机“踩坑”分享

接上一篇 openharmony 分布式相机(上),今天我们来说下如何实现分布式相机。
实现分布式相机其实很简单,正如官方介绍的一样,当被控端相机被连接成功后,可以像使用本地设备一样使用远程相机。
我们先看下效果:
上一篇已经完整的介绍了如何开发一个本地相机,对于分布式相机我们需要完成以下几个步骤。
前置条件:
两台带摄像头的设备
建议使用相同版本的 oh 系统,本案例使用 openharmony 3.2 beta5
连接在同一个网络
开发步骤:
引入设备管理(@ohos.distributedhardware.devicemanager)
通过 devicemanager 发现周边设备
通过 pin 码完成设备认证
获取和展示可信设备
在可信设备直接选择切换不同设备的摄像头
在主控端查看被控端的摄像头图像
以上描述的功能在应用开发时可以使用一张草图来表示,草图中切换设备->弹窗显示设备列表的过程,草图如下:
代码
①remotedevicemodel.ts
说明: 远程设备业务处理类,包括获取可信设备列表、获取周边设备列表、监听设备状态(上线、下线、状态变化)、监听设备连接失败、设备授信认证、卸载设备状态监听等。
代码如下:
import devicemanager from '@ohos.distributedhardware.devicemanager'import logger from './util/logger'const tag: string = 'remotedevicemodel'let subscribeid: number = -1export class remotedevicemodel {    private devicelist: array = []    private discoverlist: array = []    private callback: () => void    private authcallback: () => void    private devicemanager: devicemanager.devicemanager    constructor() {    }    public registerdevicelistcallback(bundlename : string, callback) {        if (typeof (this.devicemanager) !== 'undefined') {            this.registerdevicelistcallbackimplement(callback)            return        }        logger.info(tag, `devicemanager.createdevicemanager begin`)        try {            devicemanager.createdevicemanager(bundlename, (error, value) => {                if (error) {                    logger.info(tag, `createdevicemanager failed.`)                    return                }                this.devicemanager = value                this.registerdevicelistcallbackimplement(callback)                logger.info(tag, `createdevicemanager callback returned, error= ${error},value= ${value}`)            })        } catch (err) {            logger.error(tag, `createdevicemanager failed, code is ${err.code}, message is ${err.message}`)        }        logger.info(tag, `devicemanager.createdevicemanager end`)    }    private devicestatechangeactionoffline(device) {        if (this.devicelist.length  {            if (data === null) {                return            }            logger.info(tag, `devicefound data= ${json.stringify(data)}`)            this.devicefound(data)        })        this.devicemanager.on('discoverfail', (data) => {            logger.info(tag, `discoverfail data= ${json.stringify(data)}`)        })        this.devicemanager.on('servicedie', () => {            logger.info(tag, `servicedie`)        })        this.startdevicediscovery()    }    private devicefound(data) {        for (var i = 0;i = 0) {            logger.info(tag, `started devicediscovery`)            return        }        subscribeid = math.floor(65536 * math.random())        let info = {            subscribeid: subscribeid,            mode: devicemanager.discovermode.discover_mode_active,            medium: devicemanager.exchangemedium.coap,            freq: devicemanager.exchangefreq.high,            issameaccount: false,            iswakeremote: true,            capability: devicemanager.subscribecap.subscribe_capability_ddmp        }        logger.info(tag, `startdevicediscovery ${subscribeid}`)        try {            // todo 多次启动发现周边设备有什么影响吗?            this.devicemanager.startdevicediscovery(info)        } catch (err) {            logger.error(tag, `startdevicediscovery failed, code is ${err.code}, message is ${err.message}`)        }    }    public unregisterdevicelistcallback() {        logger.info(tag, `stopdevicediscovery $subscribeid}`)        this.devicelist = []        this.discoverlist = []        try {            this.devicemanager.stopdevicediscovery(subscribeid)        } catch (err) {            logger.error(tag, `stopdevicediscovery failed, code is ${err.code}, message is ${err.message}`)        }        this.devicemanager.off('devicestatechange')        this.devicemanager.off('devicefound')        this.devicemanager.off('discoverfail')        this.devicemanager.off('servicedie')    }    public authenticatedevice(device, extrainfo, callback) {        logger.info(tag, `authenticatedevice ${json.stringify(device)}`)        for (let i = 0; i  {                    if (err) {                        logger.error(tag, `authenticatedevice error: ${json.stringify(err)}`)                        this.authcallback = null                        return                    }                    logger.info(tag, `authenticatedevice succeed: ${json.stringify(data)}`)                    this.authcallback = callback                })            } catch (err) {                logger.error(tag, `authenticatedevice failed, code is ${err.code}, message is ${err.message}`)            }        }    }    /**     * 已认证设备列表     */    public getdevicelist(): array {        return this.devicelist    }    /**     * 发现设备列表     */    public getdiscoverlist(): array {        return this.discoverlist    }}  
getdevicelist() :获取已认证的设备列表;getdiscoverlist:发现周边设备的列表。
②devicedialog.ets
说明:通过 remotedevicemodel.getdiscoverlist() 和通过 remotedevicemodel.getdevicelist() 获取到所有周边设备列表,用户通过点击切换设备按钮弹窗显示所有设备列表信息。
import devicemanager from '@ohos.distributedhardware.devicemanager';const tag = 'devicedialog'// 分布式设备选择弹窗@customdialogexport struct devicedialog {  private controller?: customdialogcontroller // 弹窗控制器  @link devicelist: array // 设备列表  @link selectindex: number // 选中的标签  build() {    column() {      list() {        foreach(this.devicelist, (item: devicemanager.deviceinfo, index) => {          listitem() {            row() {              text(item.devicename)                .fontsize(22)                .width(350)                .fontcolor(color.black)              image(index === this.selectindex ? $r('app.media.checked') : $r('app.media.uncheck'))                .width(35)                .objectfit(imagefit.contain)            }            .height(55)            .onclick(() => {              console.info(`${tag} select device ${item.deviceid}`)              if (index === this.selectindex) {                console.info(`${tag} device not change`)              } else {                this.selectindex = index              }              this.controller.close()            })          }        }, item => item.devicename)      }      .width('100%')      .height(150)      button() {        text($r('app.string.cancel'))          .width('100%')          .height(45)          .fontsize(18)          .fontcolor(color.white)          .textalign(textalign.center)      }.onclick(() => {        this.controller.close()      })      .backgroundcolor('#ed3c13')    }    .width('100%')    .padding(20)    .backgroundcolor(color.white)    .border({      color: color.white,      radius: 20    })  }}③打开设备列表弹窗  
说明:在 index.ets 页面中,点击“切换设备”按钮即可以开启设备列表弹窗,通过 @watch(‘selectedindexchange’) 监听用户选择的设备标签,在 devices 中获取到具体的 deviceinfo 对象。
代码如下:
@state @watch('selectedindexchange') selectindex: number = 0  // 设备列表  @state devices: array = []  // 设备选择弹窗  private dialogcontroller: customdialogcontroller = new customdialogcontroller({    builder: devicedialog({      devicelist: $devices,      selectindex: $selectindex,    }),    autocancel: true,    alignment: dialogalignment.center  }) showdialog() {    console.info(`${tag} registerdevicelistcallback begin`)    distributed.registerdevicelistcallback(bundle_name, () => {      console.info(`${tag} registerdevicelistcallback callback entered`)      this.devices = []      // 添加本地设备      this.devices.push({        deviceid: constant.local_device_id,        devicename: constant.local_device_name,        devicetype: 0,        networkid: '',        range: 1 // 发现设备的距离      })      let discoverlist = distributed.getdiscoverlist()      let devicelist = distributed.getdevicelist()      let discovereddevicesize = discoverlist.length      let devicesize = devicelist.length      console.info(`${tag} discovereddevicesize:${discovereddevicesize} devicesize:${devicesize}`)      let devicetemp = discovereddevicesize > 0 ? discoverlist : devicelist      for (let index = 0; index < devicetemp.length; index++) {        this.devices.push(devicetemp[index])      }    })    this.dialogcontroller.open()    console.info(`${tag} registerdevicelistcallback end`)  }async selectedindexchange() {    console.info(`${tag} select device index ${this.selectindex}`)    let discoverlist: array = distributed.getdiscoverlist()    if (discoverlist.length  {      // 获取到相关的设备id,启动远程应用      for (var index = 0; index  1) {      let cameracount: number = cameralist.length      console.info(`${tag} camera list ${cameracount}}`)      if (this.mcurcameraindex  {        })      }  至此,分布式相机的整体流程就已实现完成。下面我们介绍下分布式相机开发中所遇到的问题。    
分布式相机问题一览
    对于开发过程中所遇到的一些坑,前面多少有简单的提到一些,这里做一次规整,也算是一次回顾。
①首次授权成功无法显示相机预览
解析:我们正常会在 mainability.ts 的 oncreate() 函数加载的时候执行申请授权,在 index.ets 页面中,当 xcomponent 组件 onload() 回调后执行初始化相机操作,代码如下:
mainability.ts:
const tag: string = '[distributedcamera]'let permissionlist: array = [    ohos.permission.media_location,    ohos.permission.read_media,    ohos.permission.write_media,    ohos.permission.camera,    ohos.permission.microphone,    ohos.permission.distributed_datasync]export default class mainability extends ability {    async oncreate(want, launchparam) {        console.info(`${tag} oncreate`)        globalthis.cameraabilitycontext = this.context        await globalthis.cameraabilitycontext.requestpermissionsfromuser(permissionlist)    }  }index.ets:// ...// 截取部分主要代码column() {          xcomponent({            id: 'componentid',            type: 'surface',            controller: this.xcomponentcontroller          }).onload(async () => {            console.info(`${tag} xcomponent onload is called`)            this.xcomponentcontroller.setxcomponentsurfacesize({              surfacewidth: resolution.default_width,              surfaceheight: resolution.default_height            })            this.surfaceid = this.xcomponentcontroller.getxcomponentsurfaceid()            console.info(`${tag} surfaceid: ${this.surfaceid}`)            await this.initcamera()          }).height('100%')            .width('100%')        }        .width('100%')        .height('75%')        .margin({          bottom: 20        })// ...  
应用启动后,调用了 requestpermissionsfromuser() 请求权限后,但未手动授权时,查看相关日志:
日志告诉我们,page 的生命周期已启动到 onshow,并且页面布局也完成了加载,xcomponent 组件回调 onload()。 但是由于还未授权,导致无法初始化相机,此时即便授权成功,也不会再进行初始化,导致相机无法启动,无预览视图。 知道原因后,我们可以有多种方式解决,重点就是在授权完成后,需要再次触发初始化相机,让相机启动才可以正常预览。
我的处理方式:
在 index.ets 页面中处理授权
定义是否已授权的标识,用于判断是否可以初始化相机
定义是否已经初始化相机标识,防止对此初始化
在 page 页面初始化函数 abouttoappear() 中请求权限,并在权限申请结果中添加初始化相机操作
xcomponent 组件回调 onload() 初始化相机操作不变
index.ets:
private isinitcamera: boolean = false // 是否已初始化相机  private ispermissions: boolean = false // 是否完成授权  async abouttoappear() {    console.info(`${tag} abouttoappear`)    globalthis.cameraabilitycontext.requestpermissionsfromuser(permissionlist).then(async (data) => {      console.info(`${tag} data permissions: ${json.stringify(data.permissions)}`)      console.info(`${tag} data authresult: ${json.stringify(data.authresults)}`)      // 判断授权是否完成      let resultcount: number = 0      for (let result of data.authresults) {        if (result === 0) {          resultcount += 1        }      }      if (resultcount === permissionlist.length) {        this.ispermissions = true      }      await this.initcamera()      // 获取缩略图      this.mcameraservice.getthumbnail(this.functionbackimpl)    })  }  
②相机应用未关闭,系统息屏后重新点亮,重新返回相机应用,无预览输出流返回
解析:从现象看,预览画面卡在息屏前的状态,需要退出应用后,重启应用才能正常预览。从日志上看没有查看到具体的原因,只是 camera_host 的数据量日志消失。
猜想:相机在系统息屏后强制关闭,需要重新加载相机才能正常预览,实现方式如下:
在 page 的 onpageshow() 回调函数中重新初始化相机。
在 page 的 onpagehide() 函数中释放相机资源,减少系统资源不必要的消耗。
index.ets:
async onpageshow() {    console.info(`${tag} onpageshow`)    await this.initcamera()  }  onpagehide() {    console.info(`${tag} onpagehide`)    this.isswitchdeviceing = false    this.isinitcamera = false    this.mcameraservice.releasecamera()  }结论: 实践验证此方法有效解决息屏后点亮返回相机无法预览的问题。  
③加载远程相机,在会话管理中添加拍照输出流,无法拍照,预览黑屏
解析:两台设备 pin 码认证通过,连接成功,在主控端选择一台被控端设备时,加载相机,流程与加载本地相机相同。
流程如下:
createcamerainput()createpreviewoutput()createphotooutput()createsession()* createsession.beginconfig()* createsession.addinput(camerainput)* createsession.addoutput(previewoutput)* createsession.addoutput(photooutput)* createsession.commitconfig()* createsession.start()   
经过排查,发现日志中返回异常 not found in supported streams,详情可以查看关联 issues。
https://gitee.com/openharmony/distributedhardware_distributed_camera/issues/i6e5zx  原因:在创建 photooutput 时需要传递支持的拍照配置信息 profile,这里的 profile 可以通过 cmeramanager.getsupportedoutputcapability() 返回的相机输出能力 cameraoutputcapability 对象获取,但远程相机设备拍照输出能力列表返回空。  
但通过查看本地相机拍照输出能力可知 dayu200 设备支持的 profile 信息:
photoprofile {format:2000,size:{width:1280,height:960}}  通过此将 photoprofile 作为远程相机设备构建拍照输出流的入参场景拍照输出流,并把此添加到拍照会话管理中,但是界面出现不支持此相机配置,最终关闭了相机,导致黑屏。 解决方案:根据此问题,目前只能根据场景判断是否需要添加拍照输出流到会话管理,对于本地相机则可以添加拍照输出流,执行拍照业务,远程相机则不添加拍照输出流,这也就不能执行拍照业务,希望社区有解决方案。  
④切换不同设备上的相机,相机预览输出流出现异常,无法显示远程相机的画面
解析: 此问题存在的原因可能有多种,这里我说下我遇到的情况。 (1)分布式连接被断开,但是因为底层机制,设备之间下线需要在一段时间内才能上报(预计 5 分钟),所以在应用层看到可以连接的远端设备,其实已经下线了,这时当然不能切换到远程相机。 (2)与问题 3 中描述的相同,因为添加了一个无法支持的拍照配置信息导致相机被关闭。
解决方案:
等待线下通知,再重新连接设备,或者等待设备自动完成重连,简单粗暴就是重启设备。
待社区反馈。
⑤相机业务在主线程执行,需要将业务移动到子线程,防止 ui 线程堵塞
解析:如题描述,目前可能存在堵塞 ui 线程的可能,需要将一些耗时的操作移动到子线程,比如预览、拍照保存图片等。 目前正在修改优化,关于 ets 的异步线程 worker 可以查看之前写的一篇关于:openharmony stage worker 多线程。
⑥远程相机预览数据传输存在 500ms 的延迟
解析:在 wifi 环境下,被控端相机将预览数据通过软总线传输到主控端显示,有 500ms 左右的延迟,此问题待排查,具体是那个环境出现的延迟。
⑦no permission for function call
解析:用户动态授予:允许不同设备间的数据(ohos.permission.distributed_datasync) 交换权限后,devicemanager.startdevicediscovery() 启动发现周边设备总会出现异常。
日志中提示:
discoverfail data= {subscribeid:26386,reason:-20007,errinfo:no permission for function call.}   
原因: 非系统应用无法使用 devicemanager,详细可查看:issues。
https://gitee.com/openharmony/distributedhardware_device_manager/issues/i6byk4  解决方案:系统应用和普通应用是通过签名来区分,那只要通过修改签名 unsgnedreleasedprofiletemplate.json 文件中的 app-feature 值为 ohos_system_app,即为系统应用。    


高通推出一款新型高端机器人平台RB5
JFZ调速触发模块原理及调整维修
笔记本液晶屏的维护技巧
智能家居控制系统详解_智能家居控制系统工作原理_智能家居控制系统有哪些
传统与新兴的较量 电视行业不断创新才是发展
鸿蒙分布式相机“踩坑”分享
AI系统冒充设计师在俄企工作参与多个图形设计项目
iPhone 12 Pro Max电池续航测试:游戏消耗拖了后腿
首个5G+工业互联网国家级大会将举行
基于小波技术的图像编码方案
PCIe中的信号补偿技术——De-emphasis
“现代版罗塞塔石碑”,MIT&谷歌大脑用AI破解失传的古代文字
凌力尔特推出高性能模数转换器LTC2259-16
冷热冲击测试箱用户常识需知
油电混合动力suv汽车有哪些品牌
未来,召之即来,芭提雅提供移动零售新解决方案
n79频段对5G网络的使用有哪些影响
BYQL-YZ型标准版扬尘监测系统,确保各项污染物排放达标
怎么样做好差压变送器的安装工作
固态离子学基础知识:阻塞电极和浓度极化