【鸿蒙南向开发】基于OpenHarmony 4.1 release版本开发的网络音乐播放器

51 篇文章 0 订阅
51 篇文章 0 订阅

ArkUI 应用媒体 网络与连接
621 0
深开鸿-孙炼 只看该作者 发表于 2024-5-20 16:18:55

前言

在智能设备应用生态中,音乐播放器用户量巨大,用户活跃度非常高。在电子消费领域,主流的音乐播放器都是播放网络音乐的场景,播放本地音乐仅是一个使用率很低的功能。

然而,OpenHarmony 自带的音乐播放器,以及 Sample 仓的示例,都是播放本地音乐的场景,缺少播放网络音乐的开发示例。

因此,本文实现了一个播放网络音乐的音乐播放器应用实例,配合音乐网站服务,就可以进行网络音乐播放。

功能

播放器功能主要包括:

1、从服务器获取歌曲信息(播放器首页)

在这里插入图片描述

2、播放控制(播放详情页)

在这里插入图片描述

3、播放列表

在这里插入图片描述

架构

应用结构及其和服务器的交互关系:

在这里插入图片描述

本例主要实现了网络歌曲播放和信息展示,用户暂不关注。

实现

0、权限

ohos.permission.INTERNET
ohos.permission.KEEP_BACKGROUND_RUNNING

1、歌曲信息查询及展示

歌曲元数据:

export default class AudioItem {
  title: string = '';
  artist: string = '';
  id: string = '0'
  isPlaying: boolean = false;

  constructor(title: string, artist: string, id:string) {
    this.title = title;
    this.artist = artist;
    this.id = id;
  }
}

歌曲列表元数据:

import AudioItem from './AudioItem';

/**
 * List item data entity.
 */
export default class PlayList {
  /**
   * Text of list item.
   */
  title: string;
  /**
   * Image of list item.
   */
  img: Resource;
  /**
   * Other resource of list item.
   */
  others?: string;
  subTitle: string
  list: AudioItem[] = []

  constructor(title: string, img: Resource, subTitle: string, list: AudioItem[], others?: string) {
    this.title = title;
    this.img = img;
    this.others = others;
    this.subTitle = subTitle;
    this.list = list;
  }
}

获取歌曲列表:

import http from '@ohos.net.http'

getListFromServer() {
    this.playLists = [];
    try {
      let httpRequest = http.createHttp()
      httpRequest.request(ServerConstants.ALL_SONGS_URL, (err: Error, data: http.HttpResponse) => {
        if (!err) {
          console.info('HttpResponse Result:' + data.result);
          let aPlayingList: AudioItem[] = Array<AudioItem>();
          const jsonObject: object = JSON.parse(data.result as string);
          Object.keys(jsonObject).forEach((key) => {
            aPlayingList.push(new AudioItem(jsonObject[key].name, jsonObject[key].singer, jsonObject[key].id));
          });
          this.playLists.push(new PlayListData('全部歌曲', $r('app.media.icon'), aPlayingList.length + '首',
            aPlayingList, ''));
        } else {
          console.info('HttpResponse error:' + JSON.stringify(err));
        }
      });
    } catch (err) {
      console.info('HttpRequest error:' + JSON.stringify(err));
    }
  }

展示歌曲列表:

              Grid() {
                ForEach(this.playLists, (item: PlayListData) => {
                  GridItem() {
                    PlayListItem({ item })
                  }
                })
              }
              .margin(12)
              .columnsTemplate('1fr 1fr 1fr')
              .columnsGap(8)
              .rowsGap(12)
              .width('90%')

使用网络 URL 展示歌曲封面:

      Column() {
        Row() {
          Text(this.item.subTitle)
            .fontSize(16)
            .margin(8)
            .fontColor(Color.White)
          Blank()
          Image($r('app.media.ic_public_play_white'))
            .width(20)
            .height(20)
            .margin(8)
        }
        .width('100%')
      }
      .borderRadius(12)
      .backgroundImage(this.item.list.length > 0 ? ServerConstants.SONG_IMAGE_URL + this.item.list[0].id : this.item.img)
      .backgroundImageSize(ImageSize.Cover)
      .width(120)
      .height(120)
      .justifyContent(FlexAlign.SpaceBetween)

2、播放控制,播放器后台任务注册

import media from '@ohos.multimedia.media';
import { BusinessError } from '@ohos.base';
import AudioItem from '../model/AudioItem';
import Logger from '../utils/Logger';
import emitter from '@ohos.events.emitter';
import ServerConstants from '../manager/ServerConstants';
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
import wantAgent, { WantAgent } from '@ohos.app.ability.wantAgent';
import common from '@ohos.app.ability.common';

export default class PlayerManager {
  private tag: string = 'PlayerManager';
  private isSeek: boolean = false;
  private avPlayer: media.AVPlayer | undefined = undefined;
  private list: AudioItem[] = [];
  private currentTime: number = 0;
  private currentDuration: number = 0;
  private item: AudioItem = new AudioItem('', '', '');
  private listPosition: number = 0;
  private state: string = ServerConstants.PLAYER_STATE_UNKNOWN;
  private listTitle: string = '';
  private emitterOptions: emitter.Options = {
    priority: emitter.EventPriority.HIGH
  };

  // 注册avplayer回调函数
  setAVPlayerCallback(avPlayer: media.AVPlayer) {
    // seek操作结果回调函数
    avPlayer.on('seekDone', (seekDoneTime: number) => {
      console.info(`PlayerManager seek succeeded, seek time is ${seekDoneTime}`);
    })
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
    avPlayer.on('error', (err: BusinessError) => {
      console.error(`Invoke PlayerManager failed, code is ${err.code}, message is ${err.message}`);
      avPlayer.reset(); // 调用reset重置资源,触发idle状态
    })
    // 状态机变化回调函数
    avPlayer.on('timeUpdate', (time: number) => {
      //console.info('AVPlayer state timeUpdate:'+time);
      this.currentTime = time;
      let eventData: emitter.EventData = {
        data: {
          "currentTime": this.currentTime,
          "currentDuration": this.currentDuration
        }
      };
      emitter.emit(ServerConstants.UPDATE_TIME_EVENT_ID, this.emitterOptions, eventData);
    })
    avPlayer.on('durationUpdate', (time: number) => {
      console.info('PlayerManager state durationUpdate:' + time);
      this.currentDuration = time;
    })
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {
      this.state = state;
      let eventData: emitter.EventData = {
        data: {
          "state": state,
        }
      };
      emitter.emit(ServerConstants.UPDATE_STATE_EVENT_ID, this.emitterOptions, eventData);
      switch (state) {
        case ServerConstants.PLAYER_STATE_IDLE: // 成功调用reset接口后触发该状态机上报
          console.info('PlayerManager state idle called.');
          avPlayer.release(); // 调用release接口销毁实例对象
          break;
        case ServerConstants.PLAYER_STATE_INITIALIZED: // avplayer 设置播放源后触发该状态上报
          console.info('PlayerManager state initialized called.');
          avPlayer.prepare();
          break;
        case ServerConstants.PLAYER_STATE_PREPARED: // prepare调用成功后上报该状态机
          console.info('PlayerManager state prepared called.');
          avPlayer.play(); // 调用播放接口开始播放
          break;
        case ServerConstants.PLAYER_STATE_PLAYING: // play成功调用后触发该状态机上报
          console.info('PlayerManager state playing called.');
          this.list[this.listPosition].isPlaying = true;
          this.startContinuousTask();
          break;
        case ServerConstants.PLAYER_STATE_PAUSED: // pause成功调用后触发该状态机上报
          console.info('PlayerManager state paused called.');
          break;
        case ServerConstants.PLAYER_STATE_COMPLETED: // 播放结束后触发该状态机上报
          console.info('PlayerManager state completed called.');
          avPlayer.stop(); //调用播放结束接口
          this.next();
          break;
        case ServerConstants.PLAYER_STATE_STOPPED: // stop接口成功调用后触发该状态机上报
          console.info('PlayerManager state stopped called.');
          this.stopContinuousTask();
          this.currentTime = 0;
          Logger.info(this.tag, 'Stop:' + this.item.title);
          avPlayer.reset(); // 调用reset接口初始化avplayer状态
          break;
        case ServerConstants.PLAYER_STATE_RELEASED:
          console.info('PlayerManager state released called.');
          break;
        default:
          console.info('PlayerManager state unknown called.');
          break;
      }
    })
  }

  /**
   * 初始化
   */
  playList(listTitle: string, list: AudioItem[], item: AudioItem): void {
    this.stop();
    if (list.length <= 0) {
      Logger.error(this.tag, 'PlayList:' + 'list length <= 0');
      return;
    }
    this.list = list;
    this.listTitle = listTitle;
    this.play(item);
  }

  getCurrentPlayList(): AudioItem[] {
    return this.list;
  }

  /**
   * 播放
   */
  resume(): void {
    if (this.state === ServerConstants.PLAYER_STATE_PAUSED) {
      if (this.avPlayer !== undefined) {
        this.avPlayer.play();
      }
    }
  }

  /**
   * 播放
   */
  play(item: AudioItem): void {
    this.stop();
    Logger.info(this.tag, 'Play finish:' + this.listPosition.toString());
    let index = -1
    if (item !== undefined) {
      index = this.list.indexOf(item)
    }
    if (-1 === index) {
      this.listPosition = 0;
    } else {
      this.listPosition = index;
    }
    Logger.info(this.tag, 'Play :' + this.listPosition.toString());
    this.item = this.list[this.listPosition]
    this.avPlayerLive(ServerConstants.PLAY_SONG_URL + this.item.id);
  }

  /**
   * 暂停
   */
  pause(): void {
    if (this.avPlayer !== undefined) {
      this.avPlayer.pause();
    }
  }

  /**
   * 停止
   */
  stop(): void {
    if (this.avPlayer !== undefined) {
      this.avPlayer.stop();
      if (this.list.length > this.listPosition) {
        this.list[this.listPosition].isPlaying = false;
      }
    }
  }

  /**
   * seek
   */
  seek(duration: number): void {
    if (this.avPlayer !== undefined && this.isSeek) {
      this.avPlayer.seek(duration);
    }
  }

  /**
   * 下一首
   */
  next(): void {
    let newPosition = 0;
    if (this.listPosition + 1 === this.list.length) {
      newPosition = 0;
    } else {
      newPosition = this.listPosition + 1;
    }
    Logger.info(this.tag, 'Play next:' + newPosition.toString());
    this.play(this.list[newPosition]);
  }

  /**
   * 上一首
   */
  previous() {
    let newPosition = 0;
    if (this.listPosition === 0) {
      newPosition = 0;
    } else {
      newPosition = this.listPosition - 1;
    }
    Logger.info(this.tag, 'Play previous:' + newPosition.toString());
    this.play(this.list[newPosition]);
  }

  //播放顺序
  setPlayMode() {

  }

  getItem(): AudioItem {
    return this.item;
  }

  getListTitle(): string {
    return this.listTitle;
  }

  getState(): string {
    return this.state;
  }

  async avPlayerLive(url: string) {
    // 创建avPlayer实例对象
    if (this.avPlayer === undefined) {
      this.avPlayer = await media.createAVPlayer();
      this.setAVPlayerCallback(this.avPlayer);
    } else {
      this.avPlayer.release();
      this.avPlayer = await media.createAVPlayer();
      this.setAVPlayerCallback(this.avPlayer);
    }
    console.info('PlayerManager state url:' + url);
    this.avPlayer.url = url;
  }

  startContinuousTask() {
    let wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: "com.example.avplayer",
          abilityName: "EntryAbility"
        }
      ],
      operationType: wantAgent.OperationType.START_ABILITY,
      requestCode: 0,
      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    try {
      wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
        try {
          backgroundTaskManager.startBackgroundRunning(AppStorage.get('APPContext') as common.UIAbilityContext,
            backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => {
            console.info("PlayerManager Operation startBackgroundRunning succeeded");
          }).catch((error: BusinessError) => {
            console.error(`PlayerManager Operation startBackgroundRunning failed. code is ${error.code} message is ${error.message}`);
          });
        } catch (error) {
          console.error(`PlayerManager Operation startBackgroundRunning failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
        }
      });
    } catch (error) {
      console.error(`PlayerManager Operation getWantAgent failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
    }
  }

  // cancel continuous task
  stopContinuousTask(): void {
    try {
      backgroundTaskManager.stopBackgroundRunning(AppStorage.get('APPContext') as common.UIAbilityContext).then(() => {
        console.info("PlayerManager Operation stopBackgroundRunning succeeded");
      }).catch((error: BusinessError) => {
        console.error(`PlayerManager Operation stopBackgroundRunning failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
      });
    } catch (error) {
      console.error(`PlayerManager Operation stopBackgroundRunning failed. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
    }
  }
}


展示播放详情:

          Column() {
            Image(ServerConstants.SONG_IMAGE_URL + this.item.id)
              .width('300vp')
              .height('300vp')
              .borderRadius('200vp')
              .margin('50vp')
              .rotate({ angle: this.rotateAngle })
              .animation({
                duration: 3600,
                curve: Curve.Linear,
                delay: 500,
                iterations: -1, // 设置-1表示动画无限循环
                playMode: PlayMode.Normal
              })
            Row() {
              Column() {
                Text(this.item.title).fontSize('18fp')
                Row() {
                  Text(this.item.artist).fontSize('16fp')
                    .fontColor('#303030')
                  Text('关注')
                    .fontSize('14fp')
                    .fontColor('#303030')
                    .backgroundColor('#f0f0f0')
                    .borderRadius('6vp')
                    .padding({ left: '4vp', right: '4vp' })
                }
              }.alignItems(HorizontalAlign.Start)

              Blank()
              Stack() {
                Image($r('app.media.ic_public_favor'))
                  .width('36vp')
                  .height('36vp')
                Text('100w+').fontSize('10fp')
                  .backgroundColor('#ffffff')
                  .margin({ left: '24vp' })
              }.alignContent(Alignment.Top)
              .margin({ right: '8vp' })

              Stack() {
                Image($r('app.media.ic_public_comments'))
                  .width('36vp')
                  .height('36vp')
                Text('10w+').fontSize('10fp')
                  .backgroundColor('#ffffff')
                  .margin({ left: '24vp' })
              }.alignContent(Alignment.Top)
            }
            .width('80%')
            .justifyContent(FlexAlign.SpaceBetween)
          }.width('100%')
          .height('100%')
          .justifyContent(FlexAlign.SpaceAround)
          .onClick(() => {
            this.contentSwitch = false;
          })


播放控制条(进度条及控制按钮):

Row() {
        Progress({ value: this.currentTime, total: this.durationTime, type: ProgressType.Linear })
          .width('80%')
          .height(30)
      }.width('100%')
      .justifyContent(FlexAlign.Center)

      Row() {
        Text(CommonUtils.formatTime(this.currentTime/1000)).fontSize('12fp')
        Text('无损').fontSize('12fp')
        Text(CommonUtils.formatTime(this.durationTime/1000)).fontSize('12fp')
      }.width('80%')
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Image($r('app.media.ic_public_list_cycle'))
          .objectFit(ImageFit.Contain)
          .width(42)
          .height(42)
          .margin({ right: 12, left: 8 })
        Image($r('app.media.ic_public_play_last'))
          .objectFit(ImageFit.Contain)
          .width(42)
          .height(42)
          .margin({ right: 12, left: 8 })
          .onClick(() => {
            this.PlayerManager.previous();
          })
        if (this.state === ServerConstants.PLAYER_STATE_PLAYING) {
          Image($r('app.media.ic_public_pause'))
            .objectFit(ImageFit.Contain)
            .width(48)
            .height(48)
            .margin({ right: 12, left: 8 })
            .onClick(() => {
              this.PlayerManager.pause();
            })
        } else {
          Image($r('app.media.ic_public_play'))
            .objectFit(ImageFit.Contain)
            .width(48)
            .height(48)
            .margin({ right: 12, left: '8vp' })
            .onClick(() => {
              this.PlayerManager.resume();
            })
        }
        Image($r('app.media.ic_public_play_next'))
          .objectFit(ImageFit.Contain)
          .width(42)
          .height(42)
          .margin({ right: 12, left: 8 })
          .onClick(() => {
            this.PlayerManager.next();
          })
        Image($r('app.media.ic_public_view_list'))
          .objectFit(ImageFit.Contain)
          .width(42)
          .height(42)
          .margin({ right: 12, left: 8 })
          .onClick(() => {
            animateTo({ duration: 350 }, () => {
              this.isShowPlayList = true;
            })
          })
      }.width('100%')
      .height(64)
      .justifyContent(FlexAlign.SpaceEvenly)

3、服务器 API

本地搭建一个音乐内容服务器,提供 Rest API,需要根据实际服务器地址修改:

  static readonly SERVER_HOST = 'http://192.168.62.240:8000/';
  /**
   * All songs URL
   */
  static readonly ALL_SONGS_URL = this.SERVER_HOST + 'all_songs';
  /**
   * English songs URL
   */
  static readonly ENGLISH_SONGS_URL = this.SERVER_HOST + 'english_songs';
  /**
   * Song Image URL
   */
  static readonly SONG_IMAGE_URL = this.SERVER_HOST + 'get_song_img/';
  /**
   * Play songs URL
   */
  static readonly PLAY_SONG_URL = this.SERVER_HOST + 'play_song/';

  static readonly SERVER_HOST = 'http://192.168.62.240:8000/';
  /**
   * All songs URL
   */
  static readonly ALL_SONGS_URL = this.SERVER_HOST + 'all_songs';
  /**
   * English songs URL
   */
  static readonly ENGLISH_SONGS_URL = this.SERVER_HOST + 'english_songs';
  /**
   * Song Image URL
   */
  static readonly SONG_IMAGE_URL = this.SERVER_HOST + 'get_song_img/';
  /**
   * Play songs URL
   */
  static readonly PLAY_SONG_URL = this.SERVER_HOST + 'play_song/';


总结
本例实现了网络音乐的播放、展示、控制功能,后续通过加入用户鉴权和信息管理、创建个性化歌单等功能,即可以构成完整的网络音乐应用。

写在最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)用来跟着学习是非常有必要的。

这份鸿蒙(HarmonyOS NEXT)文档包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。

希望这一份鸿蒙学习文档能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

获取这份完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

鸿蒙(HarmonyOS NEXT)5.0最新学习路线

在这里插入图片描述

有了路线图,怎么能没有学习文档呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档

《鸿蒙 (OpenHarmony)开发入门教学视频》

在这里插入图片描述

《鸿蒙生态应用开发V3.0白皮书》

在这里插入图片描述

《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

在这里插入图片描述

《鸿蒙开发基础》

●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
在这里插入图片描述

《鸿蒙开发进阶》

●Stage模型入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●通知与窗口管理
●多媒体技术
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来设计
●鸿蒙系统移植和裁剪定制
……
在这里插入图片描述

《鸿蒙进阶实战》

●ArkTS实践
●UIAbility应用
●网络案例
……
在这里插入图片描述

获取以上完整鸿蒙HarmonyOS学习文档,请点击→纯血版全套鸿蒙HarmonyOS学习文档

  • 29
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值