鸿蒙技术分享:敲鸿蒙木鱼,积____功德🐶🐶🐶——鸿蒙元服务开发:从入门到放弃(3)
本文是系列文章,其他文章见:敲鸿蒙木鱼,积____功德——鸿蒙元服务开发:从入门到放弃(1)敲鸿蒙木鱼,积____功德——鸿蒙元服务开发:从入门到放弃(2) 本文完整源码查看funny-widget 简介 因为工作需要,准备开发元服务,所以就想着搞一个电子木鱼的DEMO学习一下元服务以及桌面卡片的功能开发知识。 详细了解HarmonyOS的元服务,可查看官方介绍。 涉及知识点 元服务开
本文是系列文章,其他文章见:
敲鸿蒙木鱼,积____功德🐶🐶🐶——鸿蒙元服务开发:从入门到放弃(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', // 只能跳转到当前应用下的UIAbility
params: {
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', // 只能跳转到当前应用下的UIAbility
params: {
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 ability
hilog.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 resources
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
}
}
主要就是在onCreate
方法中监听playLocalSound
和playOnlineSound
两个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 = false
count: number = 0
setAVPlayerCallback(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设置网络地址来实现播放直播码流的demo
async 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******50
“ let context = getContext(this) as common.ExtensionContext;”这个已经是通过call事件拉起application,走到application的代码里了,不算卡片代码的运行环境吧?
鸿蒙技术支持
开发者你好,通过call事件去拉起还是借助卡片的能力,卡片里面本身就是受限功能
总结与体会
😅😅😅在鸿蒙系统初期开发这类小众功能真的是遍地是坑。
普通的应用界面因为有大量应用在开发,所以坑填的相对快(但也不少),像元服务、卡片、音频,这样的混合领域,坑得数量可能超出你预期,开发者在前期做开发计划时尽量保守一些,不要脑袋一热,用iOS/Android的开发经验来轻率定时间进度。
更多推荐
所有评论(0)