[经验分享] 基于OpenHarmony 4.1 release版本开发的网络音乐播放器 原创 精华

深开鸿-孙炼 显示全部楼层 发表于 2024-5-20 16:18:55

前言

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

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

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

功能

播放器功能主要包括:

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

home.jpeg

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

detail.jpeg

3、播放列表

playlist.jpeg

架构

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

image.png

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

实现

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/';

总结

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

应用完整的工程代码已提交到集成测试仓:

https://gitee.com/openharmony-sig/ostest_integration_test/tree/master/scenario/MusicPlayerOnline

将用于对OpenHarmony网络音乐播放器的场景测试,服务器源码根据测试配套需求再上传。

©著作权归作者所有,转载或内容合作请联系作者

您尚未登录,无法参与评论,登录后可以:
参与开源共建问题交流
认同或收藏高质量问答
获取积分成为开源共建先驱

Copyright   ©2023  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

返回顶部