接上一篇 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型标准版扬尘监测系统,确保各项污染物排放达标
怎么样做好差压变送器的安装工作
固态离子学基础知识:阻塞电极和浓度极化