欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 社会 > 鸿蒙技术分享:敲鸿蒙木鱼,积____功德——鸿蒙元服务开发:从入门到放弃(3)...

鸿蒙技术分享:敲鸿蒙木鱼,积____功德——鸿蒙元服务开发:从入门到放弃(3)...

2024/12/22 22:16:36 来源:https://blog.csdn.net/jhq1990/article/details/144243776  浏览:    关键词:鸿蒙技术分享:敲鸿蒙木鱼,积____功德——鸿蒙元服务开发:从入门到放弃(3)...

本文是系列文章,其他文章见:
敲鸿蒙木鱼,积____功德🐶🐶🐶——鸿蒙元服务开发:从入门到放弃(1)
敲鸿蒙木鱼,积____功德🐶🐶🐶——鸿蒙元服务开发:从入门到放弃(2)

本文完整源码查看funny-widget

简介

因为工作需要,准备开发元服务,所以就想着搞一个电子木鱼的DEMO学习一下元服务以及桌面卡片的功能开发知识。

详细了解HarmonyOS的元服务,可查看官方介绍

涉及知识点

  • 元服务开发流程
  • 加载图片
  • 播放音频
  • 开发调试
  • 组件代码在卡片和元服务间共享
  • 数据在卡片和元服务间共享

应用内卡片开发

因为元服务卡片存在音频播放问题,在咨询了官方技术支持后,确定是无法播放的。

在官方文档中看到了使用call事件拉起指定UIAbility到后台

因此使用了此方法进行音频播放功能验证。

卡片代码
@Entry
@Component
struct WidgetEventCallCard {@LocalStorageProp('formId') formId: string = '12400633174999288';build() {Column() {Row() {Column() {Button() {Text('playLocalSound').padding(16)}.onClick(() => {console.info('click playLocalSound')postCardAction(this, {action: 'call',abilityName: 'WidgetEventCallEntryAbility', // 只能跳转到当前应用下的UIAbilityparams: {formId: '12400633174999288',method: 'playLocalSound' // 在EntryAbility中调用的方法名}});console.info('after click playLocalSound')})Button() {Text('playOnlineSound').padding(16)}.onClick(() => {console.info('click playOnlineSound')postCardAction(this, {action: 'call',abilityName: 'WidgetEventCallEntryAbility', // 只能跳转到当前应用下的UIAbilityparams: {formId: '12400633174999288',method: 'playOnlineSound' // 在EntryAbility中调用的方法名}});console.info('after click playOnlineSound')})}}.width('100%').height('80%').justifyContent(FlexAlign.Center)}.width('100%').height('100%').alignItems(HorizontalAlign.Center)}
}

卡片上添加了两个按钮分别用来测试本地音频播放和在线音频播放。

Entry代码
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { promptAction, window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { rpc } from '@kit.IPCKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { media } from '@kit.MediaKit';
import { AudioManager } from '../utils/AudioManager';const TAG: string = 'WidgetEventCallEntryAbility';
const DOMAIN_NUMBER: number = 0xFF00;
const CONST_NUMBER_1: number = 1;
const CONST_NUMBER_2: number = 2;class MyParcelable implements rpc.Parcelable {num: number;str: string;constructor(num: number, str: string) {this.num = num;this.str = str;}marshalling(messageSequence: rpc.MessageSequence): boolean {messageSequence.writeInt(this.num);messageSequence.writeString(this.str);return true;}unmarshalling(messageSequence: rpc.MessageSequence): boolean {this.num = messageSequence.readInt();this.str = messageSequence.readString();return true;}
}export default class WidgetEventCallEntryAbility extends UIAbility {// 如果UIAbility第一次启动,在收到call事件后会触发onCreate生命周期回调onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {try {// 监听call事件所需的方法this.callee.on('playLocalSound', (data: rpc.MessageSequence) => {// 获取call事件中传递的所有参数hilog.info(DOMAIN_NUMBER, TAG, `playLocalSound param:  ${JSON.stringify(data.readString())}`);AudioManager.shared.playSound()return new MyParcelable(CONST_NUMBER_1, 'aaa');});this.callee.on('playOnlineSound', (data: rpc.MessageSequence) => {// 获取call事件中传递的所有参数hilog.info(DOMAIN_NUMBER, TAG, `playOnlineSound param:  ${JSON.stringify(data.readString())}`);AudioManager.shared.playOnlineSound()return new MyParcelable(CONST_NUMBER_1, 'aaa');});} catch (err) {hilog.error(DOMAIN_NUMBER, TAG, `Failed to register callee on. Cause: ${JSON.stringify(err as BusinessError)}`);}}// 进程退出时,解除监听onDestroy(): void | Promise<void> {try {this.callee.off('playLocalSound');this.callee.off('playOnlineSound');} catch (err) {hilog.error(DOMAIN_NUMBER, TAG, `Failed to register callee off. Cause: ${JSON.stringify(err as BusinessError)}`);}}onWindowStageCreate(windowStage: window.WindowStage): void {// Main window is created, set main page for this abilityhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');windowStage.loadContent('pages/Index', (err) => {if (err.code) {hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');return;}hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');});}onWindowStageDestroy(): void {// Main window is destroyed, release UI related resourceshilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');}onForeground(): void {// Ability has brought to foregroundhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');}onBackground(): void {// Ability has back to backgroundhilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');}
}

主要就是在onCreate方法中监听playLocalSoundplayOnlineSound两个call事件。

AudioManager
import { media } from '@kit.MediaKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { common } from '@kit.AbilityKit'export class AudioManager {static shared = new AudioManager()playSound() {this.log(`AudioManager playSound`)this.playLocalSound()}log(message: string) {hilog.info(0x0000, '音频', '%{public}s', `${message}`);console.info(`[音频]${message}`);}isSeek: boolean = falsecount: number = 0setAVPlayerCallback(avPlayer: media.AVPlayer) {this.log('setAVPlayerCallback')// seek操作结果回调函数avPlayer.on('seekDone', (seekDoneTime: number) => {this.log(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`)})// error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程avPlayer.on('error', (err: BusinessError) => {this.log(`avPlayer on error, code is ${err.code}, message is ${err.message}`)avPlayer.reset(); // 调用reset重置资源,触发idle状态})// 状态机变化回调函数avPlayer.on('stateChange', async (state: media.AVPlayerState, reason: media.StateChangeReason) => {this.log(`stateChange回调:${state}${reason.toString()}`)switch (state) {case 'idle': // 成功调用reset接口后触发该状态机上报console.info('AVPlayer state idle called.');avPlayer.release(); // 调用release接口销毁实例对象break;case 'initialized': // avplayer 设置播放源后触发该状态上报console.info('AVPlayer state initialized called.');avPlayer.prepare();break;case 'prepared': // prepare调用成功后上报该状态机console.info('AVPlayer state prepared called.');avPlayer.play(); // 调用播放接口开始播放break;case 'playing': // play成功调用后触发该状态机上报console.info('AVPlayer state playing called.');if (this.count !== 0) {if (this.isSeek) {console.info('AVPlayer start to seek.');avPlayer.seek(avPlayer.duration); //seek到音频末尾} else {// 当播放模式不支持seek操作时继续播放到结尾console.info('AVPlayer wait to play end.');}} else {avPlayer.pause(); // 调用暂停接口暂停播放}this.count++;break;case 'paused': // pause成功调用后触发该状态机上报console.info('AVPlayer state paused called.');avPlayer.play(); // 再次播放接口开始播放break;case 'completed': // 播放结束后触发该状态机上报console.info('AVPlayer state completed called.');avPlayer.stop(); //调用播放结束接口break;case 'stopped': // stop接口成功调用后触发该状态机上报console.info('AVPlayer state stopped called.');avPlayer.reset(); // 调用reset接口初始化avplayer状态break;case 'released':console.info('AVPlayer state released called.');break;case 'error':console.info('AVPlayer state error called.');break;default:console.info('AVPlayer state unknown called.');break;}})}async playLocalSound() {hilog.info(0x0000, '音频', '%{public}s', 'playLocalSound');console.debug(`[音频]playLocalSound`)try {// 创建avPlayer实例对象let avPlayer: media.AVPlayer = await media.createAVPlayer();this.log(`createAVPlayer success`)// 创建状态机变化回调函数this.setAVPlayerCallback(avPlayer);// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度let context = getContext(this) as common.ExtensionContext;this.log(`getContext:context=${context}`)// hilog.info(0x0000, '组件', '%{public}s', `playLocalSound:context扩展名=${context.extensionAbilityInfo.name}}`);let fileDescriptor = await context.resourceManager.getRawFd('dang.mp3');this.log(`playLocalSound:fileDescriptor.length=${fileDescriptor.length}}`)let avFileDescriptor: media.AVFileDescriptor ={ fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };this.isSeek = true; // 支持seek操作// 为fdSrc赋值触发initialized状态机上报avPlayer.fdSrc = avFileDescriptor;} catch (e) {this.log(`playLocalSound出错:${e.toString()}`)}}// 以下demo为通过url设置网络地址来实现播放直播码流的demoasync playOnlineSound() {this.log(`playOnlineSound`)// 创建avPlayer实例对象let avPlayer: media.AVPlayer = await media.createAVPlayer();this.log(`createAVPlayer success`)// 创建状态机变化回调函数this.setAVPlayerCallback(avPlayer);avPlayer.url = 'https://clemmensen.top/static/muyu.mp3';}
}

音频播放代码提供了本地和在线两个播放逻辑。

结论

期望:
在应用未运行的情况下,用卡片call方法拉起App到后台并播放音频。

实测:
播放rawfile音频失败
播放在线音频成功

向官方技术支持咨询:

鸿蒙技术支持
开发者你好,这边测试本地是有音频的敲击声啊159******50
要先杀掉应用再点;正常场景是“应用未运行的情况下”点击卡片播放音频鸿蒙技术支持
确实是的,正在内部分析中鸿蒙技术支持
开发者你好,初步结论是 let context = getContext(this) as common.ExtensionContext;
此场景中上述context获取不到,导致不能读取rawfile文件159******50
这个我清楚,日志就能看到的。问题是怎么解决呢?有没有其他方案读取rawfile?鸿蒙技术支持
当前卡片框架不支持获取context,这个确认为当前规格159******50let context = getContext(this) as common.ExtensionContext;”这个已经是通过call事件拉起application,走到application的代码里了,不算卡片代码的运行环境吧?鸿蒙技术支持
开发者你好,通过call事件去拉起还是借助卡片的能力,卡片里面本身就是受限功能
总结与体会

😅😅😅在鸿蒙系统初期开发这类小众功能真的是遍地是坑。
普通的应用界面因为有大量应用在开发,所以坑填的相对快(但也不少),像元服务、卡片、音频,这样的混合领域,坑得数量可能超出你预期,开发者在前期做开发计划时尽量保守一些,不要脑袋一热,用iOS/Android的开发经验来轻率定时间进度。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com