📢 重磅福利!参与活动赢好礼,马克杯、鼠标垫、贴纸、8月1日-12月31日等你来!
点击链接:   华为开发者学堂

简介

本文档为影音娱乐类HarmonyOS应用的架构设计实践,提供影音娱乐类应用常见的音频播控、音频文件管理、播放记录、收藏等功能,帮助开发者快速构建一款影音娱乐类应用。

  • Stage开发模型+声明式UI开发范式。
  • 按照应用设备形态,规划一个手机设备Entry类型HAP包。
  • 本实践性能优先,应用程序包大小可控,且无单独加载模块场景,业务模块包类型采用HAR包。
  • 本实践侧重音频娱乐行业。

应用布局

说明

实践应用框架代码运行图,开发者可以基于框架代码替换相关资源文件,以保证应用良好的使用体验。

  • 应用首页采用常见页面导航布局。
  • 首页导航包含“推荐”、“发现”、“喜欢”、“动态”和“我的”五个功能入口,分别对应五个功能模块,额外有一个“播放”模块(模块划分详见本实践软件视图)。

应用架构设计

模块划分

根据行业应用的功能,按照高内聚,低耦合的原则,常见影音娱乐类应用功能以及职责划分模块如下,开发者在实际设计过程中,可以根据模块的复杂程度实际情况再进一步细分:

表1 模块划分

模块名称

功能点

推荐

每日热门歌曲推荐

发现

应用官方排行榜

喜欢

收藏的喜欢歌曲列表

动态

音乐评论和分享

我的

个人信息

播放

音乐播放

软件视图设计

应用分层模块类型划分指导,参见分层模块化实践

产品定制层:本应用只涉及手机端,设计为一个HAP,包含页面框架、导航、手机独有资源等。

基础特性层:将“推荐”、“音频文件管理”、“发现”、“喜欢”、“动态”、“我的”、“播放” 等功能模块打包为HAR包被上层产品组件引用。

公共能力层:将“文件管理”、“网络交互”、“DFX”等基础公共模块打包为HAR包被上层业务组件引用。

组件间依赖说明:上层组件可依赖下层,不建议跨层依赖、反向依赖、横向依赖。

图1 软件视图

逻辑视图设计

根据本应用功能的模块以及依赖,分解对基础服务以及三方的依赖,逻辑视图如下:

图2 逻辑视图

公共关键技术方案

介绍行业应用通用的公共关键技术,详见 公共关键技术方案

行业关键技术方案

音频播控技术方案

功能设计

提供音频文件的播放、暂停、跳转、停止等能力,支持后台、锁屏播放。页面路径:首页->音频播放,如下图所示:

图3 音频播放功能

方案设计

  • 使用Media Kit(媒体服务)实现音频播放功能,实现播放、暂停、跳转、停止能力。
  • 使用AVSession Kit(音视频播控服务),实现后台播放或熄屏播放。

代码参考

代码参见音乐播放暂停实现

播放历史、收藏技术方案

功能设计

提供播放历史查看、清空,歌曲收藏、取消收藏能力;数据在端侧本地存储闭环。页面路径:首页->播放历史/收藏,如下图所示:

图4 音频播放功能

方案设计

  • 使用ArkData(方舟数据管理)关系型数据库实现本地数据的存储管理。
  • 用户行为数据属于个人敏感数据,数据库开启加密保护。

代码参考

代码参见播放历史和收藏

应用框架代码

说明

本篇代码非应用的全量代码,只包括应用的部分框架代码。

框架代码中登录验证,只是UI演示。

代码运行环境

软件要求

  • DevEco Studio版本:DevEco Studio 5.0.1 Release及以上。
  • HarmonyOS SDK版本:HarmonyOS 5.0.1 Release SDK及以上。

硬件要求

  • 设备类型:华为手机。
  • HarmonyOS系统:HarmonyOS 5.0.1 Release及以上。

环境搭建

历史工程迁移

开发者使用HarmonyOS 5.0.1 Release及以上版本导入代码,使用一体化的历史工程迁移能力,帮助开发者快速完成工程转换。

代码结构解读

本篇代码非应用的全量代码,只包括脱敏后的应用框架代码,开发者可以通过链接下载全量的框架代码。


  1. entry/src/main/ets/
  2. ├── components
  3. │ └── PlayerNav.ets // 播放栏样式
  4. ├── constants
  5. │ ├── EventConstants.ets // 自定义事件名称
  6. │ ├── Index.ets // SONG_KEY常量
  7. │ └── MusicConstants.ets // 歌单、评论等数据
  8. ├── entryability
  9. │ └── EntryAbility.ets
  10. ├── entryformability
  11. │ └── EntryFormAbility.ets
  12. ├── models
  13. │ ├── Index.ets // 导航栏结构
  14. │ ├── Music.ets // 音乐结构
  15. │ └── PlayState.ets // 传输数据的结构
  16. ├── pages
  17. │ ├── find
  18. │ │ └── Find.ets // “发现”页
  19. │ ├── mine
  20. │ │ └── Mine.ets // “我的”页
  21. │ ├── moment
  22. │ │ └── Moment.ets // “动态”页
  23. │ ├── play
  24. │ │ └── Play.ets // 音乐播放页
  25. │ ├── recommend
  26. │ │ └── Recommend.ets // "推荐"页
  27. │ ├── like
  28. │ │ └── Like.ets // 喜欢页
  29. │ └── Index.ets // 下导航栏、背景设置
  30. └── utils
  31. ├── AVPlayerManager.ets // 播放器创建
  32. ├── AVSessionManager.ets // 媒体会话
  33. ├── ImageSave.ets // 图片保存
  34. └── SongManager.ets // 歌曲管理

音乐播放暂停实现


  1. // entry/src/main/ets/utils/AVPlayerManager.ets
  2. // 单歌播放
  3. static async singlePlay(song: songItemType) {
  4. AVPlayerManager.startBackgroundTask()
  5. // 添加到播放列表中,再进行播放
  6. const isList = AVPlayerManager.currentSong.playList.some(
  7. (item: songItemType) => item.id === song.id
  8. )
  9. if (isList) {
  10. // 是不是正在播放的
  11. if (AVPlayerManager.player!.url === song.url) {
  12. // 重新播放(切换当前播放索引的歌曲)
  13. AVPlayerManager.player?.play()
  14. AVPlayerManager.currentSong.isPlay = true
  15. } else {
  16. // 切换歌曲(更新播放索引切换歌曲)
  17. AVPlayerManager.currentSong.playIndex =
  18. AVPlayerManager.currentSong.playList.findIndex(
  19. (item: songItemType) => item.id === song.id
  20. )
  21. AVPlayerManager.changePlay()
  22. }
  23. } else {
  24. AVPlayerManager.currentSong.playList.unshift(song)
  25. AVPlayerManager.currentSong.playIndex = 0
  26. AVPlayerManager.changePlay()
  27. }
  28. AppStorage.setOrCreate<PlayStateType>(SONG_KEY, AVPlayerManager.currentSong) // 更新全局状态
  29. AVSessionManager.setAVPlaybackState() // 更新状态
  30. }
  31. // 暂停播放
  32. static pause() {
  33. AVPlayerManager.player!.pause()
  34. // 更新播放器状态
  35. AVPlayerManager.currentSong.isPlay = false
  36. AVSessionManager.setAVPlaybackState() // 更新状态
  37. }
  38. // 上一首
  39. static prevPlay() {
  40. // 如果是随机播放
  41. if (AVPlayerManager.currentSong.playMode === "random" && AVPlayerManager.currentSong.playList.length > 1) {
  42. let index = -1
  43. do {
  44. index = Math.floor(Math.random() * AVPlayerManager.currentSong.playList.length)
  45. } while (index === AVPlayerManager.currentSong.playIndex)
  46. AVPlayerManager.currentSong.playIndex = index
  47. } else {
  48. AVPlayerManager.currentSong.playIndex--
  49. // 有可能减到负数
  50. AVPlayerManager.currentSong.playIndex =
  51. (AVPlayerManager.currentSong.playIndex + AVPlayerManager.currentSong.playList.length) %
  52. AVPlayerManager.currentSong.playList.length
  53. }
  54. AVPlayerManager.singlePlay(AVPlayerManager.currentSong.playList[AVPlayerManager.currentSong.playIndex])
  55. }
  56. // 下一首
  57. static nextPlay(repeat?: boolean) {
  58. if (!AVPlayerManager.currentSong.playList.length) {
  59. return
  60. }
  61. if (!repeat) {
  62. if (AVPlayerManager.currentSong.playMode === "random" && AVPlayerManager.currentSong.playList.length > 1) {
  63. let index = 0
  64. do {
  65. index = Math.floor(Math.random() * AVPlayerManager.currentSong.playList.length)
  66. } while (index === AVPlayerManager.currentSong.playIndex)
  67. AVPlayerManager.currentSong.playIndex = index
  68. } else {
  69. AVPlayerManager.currentSong.playIndex++
  70. // 有可能减到负数
  71. AVPlayerManager.currentSong.playIndex =
  72. (AVPlayerManager.currentSong.playIndex + AVPlayerManager.currentSong.playList.length) %
  73. AVPlayerManager.currentSong.playList.length
  74. }
  75. }
  76. AVPlayerManager.singlePlay(AVPlayerManager.currentSong.playList[AVPlayerManager.currentSong.playIndex])
  77. }

音乐后台播放实现


  1. // entry/src/main/ets/utils/AVPlayerManager.ets
  2. static async startBackgroundTask() {
  3. if (AVSessionManager.session.sessionId) {
  4. // 如果有会话,则不用开启后台任务
  5. return
  6. }
  7. try {
  8. let wantAgentInfo: wantAgent.WantAgentInfo = {
  9. wants: [
  10. {
  11. bundleName: "com.example.mymusic",
  12. abilityName: "com.example.mymusic.EntryAbility"
  13. }
  14. ],
  15. actionType: wantAgent.OperationType.START_ABILITY,
  16. requestCode: 0,
  17. wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
  18. };
  19. const want = await wantAgent.getWantAgent(wantAgentInfo)
  20. await backgroundTaskManager.startBackgroundRunning(
  21. getContext(),
  22. backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
  23. want
  24. )
  25. } catch (e) {
  26. AlertDialog.show({ message: e.message })
  27. }
  28. }

播放历史和收藏


  1. // entry/src/main/ets/pages/play/Play.ets
  2. build() {
  3. Stack({ alignContent: Alignment.Bottom }) {
  4. // 播放
  5. Stack() {
  6. // 变色背景
  7. Image(this.playState.img)
  8. .width('100%')
  9. .height('100%')
  10. .blur(1000)
  11. // 内容
  12. Column() {
  13. // 播放界面
  14. Column() {
  15. //头部控制
  16. Row(){
  17. Image($r('app.media.chevron_down'))
  18. .width(24)
  19. .height(24)
  20. Row(){
  21. Text()
  22. .backgroundColor('#465065')
  23. .height(6)
  24. .width(6)
  25. .borderRadius('50%')
  26. Text()
  27. .width(4)
  28. Text()
  29. .backgroundColor('#DFE0E4')
  30. .height(6)
  31. .width(12)
  32. .borderRadius('50%')
  33. }
  34. Image($r('app.media.people_wifi'))
  35. .width(26)
  36. .height(22)
  37. }
  38. .height(48)
  39. .margin({
  40. top: AppStorage.get<number>('topHeight'),
  41. })
  42. .width('1100px')
  43. .justifyContent(FlexAlign.SpaceBetween)
  44. // 图片
  45. Image(this.playState.img)
  46. .height('1100px')
  47. .width('1100px')
  48. .objectFit(ImageFit.Cover)
  49. .borderRadius(10)
  50. .margin({
  51. top:24,
  52. bottom:24
  53. })
  54. // 歌曲信息
  55. Row(){
  56. Column() {
  57. Text(this.playState.name)
  58. .fontSize(25)
  59. .fontWeight(FontWeight.Bold)
  60. .fontColor('#E1E2E7')
  61. Text(this.playState.author)
  62. .fontSize(18)
  63. .fontColor('#989EAD')
  64. }
  65. .alignItems(HorizontalAlign.Start)
  66. Stack(){
  67. Image($r('app.media.ic_like'))
  68. .width(24)
  69. Text('1w+')
  70. .fontColor(Color.White)
  71. .fontSize(10)
  72. .padding({
  73. bottom:20,
  74. left:20
  75. })
  76. .fontWeight(600)
  77. .width(40)
  78. .height(40)
  79. }
  80. .width(40)
  81. .height(40)
  82. }.justifyContent(FlexAlign.SpaceBetween)
  83. .width('1100px')
  84. .height(47)
  85. .margin({
  86. bottom:84
  87. })
  88. // 操作
  89. Row() {
  90. //歌单
  91. Image($r('app.media.ic_song_list'))
  92. .fillColor(Color.White)
  93. .width(28)
  94. .onClick(() => {
  95. this.panelHeight = '100%'
  96. })
  97. //下载
  98. Image($r('app.media.ic_download_o'))
  99. .fillColor(Color.White)
  100. .width(30)
  101. //铃声
  102. Image($r('app.media.ic_sing'))
  103. .fillColor(Color.White)
  104. .width(30)
  105. //其他
  106. Image($r('app.media.ic_other'))
  107. .fillColor(Color.White)
  108. .width(30)
  109. }
  110. .width('100%')
  111. .justifyContent(FlexAlign.SpaceBetween)
  112. .margin({
  113. bottom:10
  114. })
  115. // 播放
  116. Column() {
  117. // 进度条
  118. Slider({
  119. value: this.playState.time,
  120. min: 0,
  121. max: this.playState.duration,
  122. style:SliderStyle.InSet,
  123. })
  124. .blockBorderWidth(0)
  125. .blockSize({
  126. width:0.2,
  127. height:0.2
  128. })
  129. .trackThickness(5)
  130. .blockColor(Color.White)
  131. .blockBorderColor(Color.White)
  132. .selectedColor(Color.White)
  133. .trackColor('#ccc5c5c5')
  134. .onChange((val) => {
  135. AVPlayerManager.player!.seek(val)
  136. }).margin({
  137. bottom:-10,
  138. left:-7,
  139. right:-7
  140. })
  141. //时间
  142. Row() {
  143. Text(this.number2time(this.playState.time))
  144. .fontSize(12)
  145. .fontColor('#A5B4B1')
  146. Text(this.number2time(this.playState.duration))
  147. .fontSize(12)
  148. .fontColor('#A5B4B1')
  149. }
  150. .justifyContent(FlexAlign.SpaceBetween)
  151. .width('100%')
  152. .margin({
  153. bottom:10
  154. })
  155. // 切换
  156. Row() {
  157. Image($r('app.media.ic_prev'))
  158. .fillColor(Color.White)
  159. .width(40)
  160. .fillColor(Color.White)
  161. .onClick(() => {
  162. AVPlayerManager.prevPlay()
  163. })
  164. // 播放按钮
  165. Image(this.playState.isPlay ? $r('app.media.ic_paused') : $r('app.media.ic_play'))
  166. .fillColor(Color.White)
  167. .width(80)
  168. .objectFit(ImageFit.Contain)
  169. .onClick(() => {
  170. if (!this.playState.isPlay) {
  171. AVPlayerManager.singlePlay(this.playState.playList[this.playState.playIndex])
  172. this.animatorResult.play()
  173. } else {
  174. AVPlayerManager.pause()
  175. this.animatorResult.pause()
  176. }
  177. })
  178. // 下一首
  179. Image($r('app.media.ic_next'))
  180. .fillColor(Color.White)
  181. .width(40)
  182. .fillColor(Color.White)
  183. .onClick(() => {
  184. AVPlayerManager.nextPlay()
  185. })
  186. }
  187. .width('100%')
  188. .padding({
  189. bottom: 24
  190. })
  191. .justifyContent(FlexAlign.SpaceAround)
  192. }
  193. .width('100%')
  194. }
  195. .layoutWeight(1)
  196. .width('1100px')
  197. }
  198. }
  199. .backgroundColor(Color.Transparent)
  200. // 列表
  201. Column() {
  202. Column() {
  203. }
  204. .width('100%')
  205. .layoutWeight(1)
  206. .onClick(() => {
  207. this.panelHeight = '0%'
  208. })
  209. Column() {
  210. Row() {
  211. Row() {
  212. Image($r('app.media.ic_play'))
  213. .width(20)
  214. .fillColor('#ff5186')
  215. }
  216. .width(50)
  217. .aspectRatio(1)
  218. .justifyContent(FlexAlign.Center)
  219. Row({ space: 8 }) {
  220. Text(`播放列表 (${this.playState.playList?.length})`)
  221. .fontColor(Color.White)
  222. .fontSize(14)
  223. }
  224. .layoutWeight(1)
  225. Image($r('app.media.ic_close'))
  226. .fillColor('#ffa49a9a')
  227. .width(24)
  228. .height(24)
  229. .margin({ right: 16 })
  230. .onClick(() => {
  231. this.panelHeight = '0%'
  232. })
  233. }
  234. .width('100%')
  235. .backgroundColor('#ff353333')
  236. .padding(8)
  237. .border({
  238. width: { bottom: 1 },
  239. color: '#12ec5c87'
  240. })
  241. .borderRadius({
  242. topLeft: 4,
  243. topRight: 4
  244. })
  245. // 播放列表
  246. List() {
  247. ForEach(this.playState.playList, (item: songItemType, index: number) => {
  248. ListItem() {
  249. Row() {
  250. Row() {
  251. Text((index + 1).toString())
  252. .fontColor('#ffa49a9a')
  253. }
  254. .width(50)
  255. .aspectRatio(1)
  256. .justifyContent(FlexAlign.Center)
  257. // 列表
  258. Row({ space: 10 }) {
  259. Column() {
  260. Text(item.name)
  261. .fontSize(14)
  262. .fontColor(this.playState.playIndex === index ? $r('app.color.primary_light') : '#ffa49a9a')
  263. Text(item.author)
  264. .fontSize(12)
  265. .fontColor(this.playState.playIndex === index ? $r('app.color.primary_light') : Color.Gray)
  266. }
  267. .layoutWeight(1)
  268. .alignItems(HorizontalAlign.Start)
  269. .justifyContent(FlexAlign.Center)
  270. }
  271. .layoutWeight(1)
  272. Image($r('app.media.ic_more'))
  273. .width(24)
  274. .height(24)
  275. .margin({ right: 16 })
  276. .fillColor(Color.Gray)
  277. }
  278. .alignItems(VerticalAlign.Center)
  279. }
  280. .onClick(() => {
  281. AVPlayerManager.singlePlay(item)
  282. })
  283. .swipeAction({
  284. end: this.deleteButton(index)
  285. })
  286. .border({
  287. width: { bottom: 1 },
  288. color: '#12ec5c87'
  289. })
  290. })
  291. }
  292. .layoutWeight(1)
  293. .backgroundColor('#ff353333')
  294. }
  295. .height(400)
  296. }
  297. .height(this.panelHeight)
  298. .animation({
  299. duration: 300
  300. })
  301. }
  302. .width('100%')
  303. .height('100%')
  304. .backgroundColor(Color.Transparent)
  305. .opacity(this.opacityNum)
  306. }

代码下载链接

影音娱乐示例代码.zip

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐