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

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

本文完整源码查看funny-widget

简介

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

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

涉及知识点

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

应用内卡片开发

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

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

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

卡片代码
typescript 复制代码
@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代码
typescript 复制代码
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方法中监听playLocalSoundplayOnlineSound两个call事件。

AudioManager
typescript 复制代码
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音频失败

播放在线音频成功

向官方技术支持咨询:

typescript 复制代码
鸿蒙技术支持
开发者你好,这边测试本地是有音频的敲击声啊

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的开发经验来轻率定时间进度。

相关推荐
前端菜鸟日常2 小时前
鸿蒙Arkts开发飞机大战小游戏,包含无敌模式,自动射弹,暂停和继续
华为·harmonyos
HMS Core3 小时前
【FAQ】HarmonyOS SDK 闭源开放能力 —Account Kit(3)
华为·harmonyos
前端菜鸟日常21 小时前
鸿蒙版(ArkTs) 贪吃蛇,包含无敌模式 最高分 暂停和继续功能
华为·harmonyos
鸿蒙布道师1 天前
鸿蒙NEXT开发数值工具类(TS)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
塞尔维亚大汉1 天前
鸿蒙南向开发 ——轻量系统芯片移植指南(二)
物联网·嵌入式·harmonyos
别说我什么都不会1 天前
OpenHarmony内核系统调用hats测试用例编写指南
物联网·嵌入式·harmonyos
90后的晨仔1 天前
鸿蒙ArkTS是如何实现并发的?
harmonyos
鸿蒙布道师1 天前
鸿蒙NEXT开发日期工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
HMSCore2 天前
在应用内购票、寄件时,如何一键填充所需信息?
harmonyos
HarmonyOS_SDK2 天前
在应用内购票、寄件时,如何一键填充所需信息?
harmonyos