在鸿蒙HarmonyOS 5中HarmonyOS应用开发实现QQ音乐风格的播放功能

Source

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似QQ音乐的音乐播放功能,包括播放控制、进度条、歌词显示等核心功能。

1. 项目基础配置

首先在config.json中添加必要的权限和能力声明:

"abilities": [
  {
    "name": "MainAbility",
    "type": "page",
    "launchType": "standard",
    "backgroundModes": ["audio"]
  }
],
"reqPermissions": [
  {
    "name": "ohos.permission.READ_MEDIA"
  },
  {
    "name": "ohos.permission.MEDIA_LOCATION"
  },
  {
    "name": "ohos.permission.INTERNET"
  }
]

2. 音频服务实现

创建音频播放服务,使用@ohos.multimedia.audio模块:

// AudioPlayerService.ets
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';

export class AudioPlayerService {
  private audioPlayer: audio.AudioPlayer | null = null;
  private currentState: PlayerState = PlayerState.IDLE;
  private currentPosition: number = 0;
  private duration: number = 0;
  private currentSong: Song | null = null;
  
  // 初始化音频播放器
  async initAudioPlayer() {
    const audioManager = audio.getAudioManager();
    const audioStreamInfo: audio.AudioStreamInfo = {
      samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
      channels: audio.AudioChannel.CHANNEL_2,
      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
    };
    
    const audioPlayerOptions: audio.AudioPlayerOptions = {
      streamInfo: audioStreamInfo,
      playerType: audio.PlayerType.MEDIA_PLAYER
    };
    
    this.audioPlayer = await audioManager.createAudioPlayer(audioPlayerOptions);
    
    // 注册事件监听
    this.audioPlayer.on('play', () => {
      this.currentState = PlayerState.PLAYING;
    });
    
    this.audioPlayer.on('pause', () => {
      this.currentState = PlayerState.PAUSED;
    });
    
    this.audioPlayer.on('stop', () => {
      this.currentState = PlayerState.STOPPED;
    });
    
    this.audioPlayer.on('timeUpdate', (time: number) => {
      this.currentPosition = time;
    });
    
    this.audioPlayer.on('durationUpdate', (duration: number) => {
      this.duration = duration;
    });
    
    this.audioPlayer.on('error', (error: BusinessError) => {
      console.error(`AudioPlayer error: ${JSON.stringify(error)}`);
      this.currentState = PlayerState.ERROR;
    });
  }
  
  // 播放音乐
  async play(song: Song) {
    if (!this.audioPlayer) {
      await this.initAudioPlayer();
    }
    
    this.currentSong = song;
    
    // 设置音频源
    if (song.isLocal) {
      const file = await fs.open(song.url, fs.OpenMode.READ_ONLY);
      await this.audioPlayer.setSource(file.fd);
    } else {
      await this.audioPlayer.setSource(song.url);
    }
    
    await this.audioPlayer.play();
  }
  
  // 暂停播放
  async pause() {
    if (this.audioPlayer && this.currentState === PlayerState.PLAYING) {
      await this.audioPlayer.pause();
    }
  }
  
  // 继续播放
  async resume() {
    if (this.audioPlayer && this.currentState === PlayerState.PAUSED) {
      await this.audioPlayer.play();
    }
  }
  
  // 停止播放
  async stop() {
    if (this.audioPlayer) {
      await this.audioPlayer.stop();
      this.currentPosition = 0;
    }
  }
  
  // 跳转到指定位置
  async seekTo(position: number) {
    if (this.audioPlayer) {
      await this.audioPlayer.seek(position);
    }
  }
  
  // 获取当前播放状态
  getCurrentState(): PlayerState {
    return this.currentState;
  }
  
  // 获取当前播放位置
  getCurrentPosition(): number {
    return this.currentPosition;
  }
  
  // 获取歌曲总时长
  getDuration(): number {
    return this.duration;
  }
  
  // 获取当前播放歌曲
  getCurrentSong(): Song | null {
    return this.currentSong;
  }
}

export enum PlayerState {
  IDLE = 'idle',
  PLAYING = 'playing',
  PAUSED = 'paused',
  STOPPED = 'stopped',
  ERROR = 'error'
}

export interface Song {
  id: string;
  title: string;
  artist: string;
  album: string;
  url: string;
  coverUrl: string;
  duration: number;
  isLocal: boolean;
  lyrics?: LyricLine[];
}

export interface LyricLine {
  time: number; // 毫秒
  text: string;
}

3. 播放器UI实现

创建主播放器界面:

// PlayerPage.ets
import { AudioPlayerService, PlayerState, Song } from './AudioPlayerService';

@Entry
@Component
struct PlayerPage {
  private audioService: AudioPlayerService = new AudioPlayerService();
  @State currentSong: Song | null = null;
  @State playerState: PlayerState = PlayerState.IDLE;
  @State currentPosition: number = 0;
  @State duration: number = 0;
  @State showLyrics: boolean = true;
  @State currentLyricIndex: number = -1;
  
  // 模拟歌曲数据
  private songs: Song[] = [
    {
      id: "1",
      title: "示例歌曲1",
      artist: "歌手1",
      album: "专辑1",
      url: "https://example.com/song1.mp3",
      coverUrl: "resources/cover1.jpg",
      duration: 240000,
      isLocal: false,
      lyrics: [
        { time: 0, text: "[00:00.00] 这是第一句歌词" },
        { time: 5000, text: "[00:05.00] 这是第二句歌词" },
        // 更多歌词...
      ]
    },
    // 更多歌曲...
  ];
  
  onPageShow() {
    this.audioService.initAudioPlayer();
    this.loadSong(this.songs[0]);
    
    // 定时更新UI状态
    setInterval(() => {
      this.currentPosition = this.audioService.getCurrentPosition();
      this.duration = this.audioService.getDuration();
      this.playerState = this.audioService.getCurrentState();
      this.currentSong = this.audioService.getCurrentSong();
      this.updateCurrentLyric();
    }, 500);
  }
  
  loadSong(song: Song) {
    this.audioService.play(song);
  }
  
  // 更新当前歌词
  updateCurrentLyric() {
    if (!this.currentSong || !this.currentSong.lyrics) {
      this.currentLyricIndex = -1;
      return;
    }
    
    const lyrics = this.currentSong.lyrics;
    for (let i = 0; i < lyrics.length; i++) {
      if (this.currentPosition < lyrics[i].time) {
        this.currentLyricIndex = i - 1;
        return;
      }
    }
    this.currentLyricIndex = lyrics.length - 1;
  }
  
  // 格式化时间显示
  formatTime(ms: number): string {
    const totalSeconds = Math.floor(ms / 1000);
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;
    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
  }
  
  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Image($r('app.media.ic_back'))
          .width(24)
          .height(24)
          .margin({ left: 12 })
          .onClick(() => {
            // 返回上一页
          })
        
        Text(this.currentSong?.title || '未播放')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
        
        Image($r('app.media.ic_more'))
          .width(24)
          .height(24)
          .margin({ right: 12 })
      }
      .width('100%')
      .height(56)
      .alignItems(VerticalAlign.Center)
      
      // 专辑封面
      Column() {
        if (this.currentSong) {
          Image(this.currentSong.coverUrl || $r('app.media.default_cover'))
            .width(300)
            .height(300)
            .borderRadius(150)
            .margin({ top: 30 })
            .animation({ duration: 1000, curve: Curve.EaseInOut })
        }
      }
      .width('100%')
      .height('40%')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      
      // 歌曲信息
      Column() {
        Text(this.currentSong?.title || '--')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 8 })
        
        Text(this.currentSong?.artist || '--')
          .fontSize(16)
          .fontColor('#999999')
      }
      .width('100%')
      .margin({ top: 20, bottom: 20 })
      .alignItems(HorizontalAlign.Center)
      
      // 进度条
      Column() {
        Row() {
          Text(this.formatTime(this.currentPosition))
            .fontSize(12)
            .fontColor('#666666')
            .width('15%')
          
          Slider({
            value: this.currentPosition,
            min: 0,
            max: this.duration || 1,
            step: 1000,
            style: SliderStyle.OutSet
          })
            .blockColor('#07C160')
            .trackColor('#E5E5E5')
            .selectedColor('#07C160')
            .layoutWeight(1)
            .onChange((value: number) => {
              this.audioService.seekTo(value);
            })
          
          Text(this.formatTime(this.duration))
            .fontSize(12)
            .fontColor('#666666')
            .width('15%')
        }
        .width('90%')
        .margin({ top: 10 })
      }
      .width('100%')
      
      // 歌词显示区域
      if (this.showLyrics && this.currentSong?.lyrics) {
        Scroll() {
          Column() {
            ForEach(this.currentSong.lyrics, (lyric: LyricLine, index: number) => {
              Text(lyric.text.split('] ')[1] || lyric.text)
                .fontSize(this.currentLyricIndex === index ? 18 : 16)
                .fontColor(this.currentLyricIndex === index ? '#07C160' : '#666666')
                .textAlign(TextAlign.Center)
                .margin({ top: 10, bottom: 10 })
                .width('100%')
            })
          }
          .width('100%')
        }
        .height('20%')
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .edgeEffect(EdgeEffect.Spring)
      }
      
      // 控制按钮
      Row() {
        Image($r('app.media.ic_prev'))
          .width(36)
          .height(36)
          .margin({ right: 30 })
          .onClick(() => {
            // 上一首
          })
        
        if (this.playerState === PlayerState.PLAYING) {
          Image($r('app.media.ic_pause'))
            .width(48)
            .height(48)
            .onClick(() => {
              this.audioService.pause();
            })
        } else {
          Image($r('app.media.ic_play'))
            .width(48)
            .height(48)
            .onClick(() => {
              if (this.playerState === PlayerState.PAUSED) {
                this.audioService.resume();
              } else if (this.currentSong) {
                this.audioService.play(this.currentSong);
              }
            })
        }
        
        Image($r('app.media.ic_next'))
          .width(36)
          .height(36)
          .margin({ left: 30 })
          .onClick(() => {
            // 下一首
          })
      }
      .width('100%')
      .margin({ top: 20, bottom: 30 })
      .justifyContent(FlexAlign.Center)
      .alignItems(VerticalAlign.Center)
      
      // 底部控制栏
      Row() {
        Image($r('app.media.ic_favorite'))
          .width(24)
          .height(24)
          .margin({ right: 30 })
        
        Image($r('app.media.ic_download'))
          .width(24)
          .height(24)
          .margin({ right: 30 })
        
        Image($r('app.media.ic_lyrics'))
          .width(24)
          .height(24)
          .onClick(() => {
            this.showLyrics = !this.showLyrics;
          })
        
        Image($r('app.media.ic_playlist'))
          .width(24)
          .height(24)
          .margin({ left: 30 })
      }
      .width('100%')
      .height(60)
      .justifyContent(FlexAlign.Center)
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }
}

4. 歌词解析功能

添加歌词解析工具类:

// LyricParser.ets
export class LyricParser {
  static parseLyric(lyricText: string): LyricLine[] {
    const lines = lyricText.split('\n');
    const result: LyricLine[] = [];
    
    const timeRegex = /$$(\d{2}):(\d{2})\.(\d{2,3})$$/;
    
    for (const line of lines) {
      const matches = timeRegex.exec(line);
      if (matches) {
        const minutes = parseInt(matches[1]);
        const seconds = parseInt(matches[2]);
        const milliseconds = parseInt(matches[3].length === 2 ? matches[3] + '0' : matches[3]);
        
        const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds;
        const text = line.replace(timeRegex, '').trim();
        
        if (text) {
          result.push({
            time,
            text: `[${matches[0].substring(1)}] ${text}`
          });
        }
      }
    }
    
    // 按时间排序
    result.sort((a, b) => a.time - b.time);
    
    return result;
  }
}

5. 播放列表实现

创建播放列表组件:

// PlaylistDialog.ets
@Component
export struct PlaylistDialog {
  private songs: Song[];
  @Link currentSong: Song | null;
  private audioService: AudioPlayerService;
  @State searchText: string = '';
  
  build() {
    Column() {
      // 搜索框
      Row() {
        TextInput({ placeholder: '搜索播放列表' })
          .height(40)
          .layoutWeight(1)
          .onChange((value: string) => {
            this.searchText = value;
          })
      }
      .padding(10)
      .borderRadius(20)
      .backgroundColor('#F5F5F5')
      .margin({ top: 10, bottom: 10 })
      
      // 歌曲列表
      List() {
        ForEach(this.getFilteredSongs(), (song: Song) => {
          ListItem() {
            Row() {
              Image(song.coverUrl || $r('app.media.default_cover'))
                .width(50)
                .height(50)
                .borderRadius(5)
                .margin({ right: 10 })
              
              Column() {
                Text(song.title)
                  .fontSize(16)
                  .fontColor(this.currentSong?.id === song.id ? '#07C160' : '#000000')
                Text(song.artist)
                  .fontSize(12)
                  .fontColor('#999999')
              }
              .layoutWeight(1)
              
              if (this.currentSong?.id === song.id) {
                Image($r('app.media.ic_playing'))
                  .width(20)
                  .height(20)
              }
            }
            .padding(10)
            .width('100%')
          }
          .onClick(() => {
            this.audioService.play(song);
          })
        })
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('70%')
    .backgroundColor(Color.White)
    .borderRadius(10)
  }
  
  private getFilteredSongs(): Song[] {
    if (!this.searchText) {
      return this.songs;
    }
    return this.songs.filter(song => 
      song.title.includes(this.searchText) || 
      song.artist.includes(this.searchText)
    );
  }
}

6. 后台播放与通知控制

实现后台播放和通知控制:

// 在AudioPlayerService中添加
import notification from '@ohos.notification';
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';

export class AudioPlayerService {
  // ...之前代码
  
  private registerBackgroundTask() {
    let bgTask = {
      mode: backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
      isPersisted: true,
      bundleName: "com.example.musicapp", // 替换为你的应用包名
      abilityName: "MainAbility"
    };
    
    backgroundTaskManager.startBackgroundRunning(bgTask)
      .then(() => console.info("Background task registered"))
      .catch(err => console.error("Background task error: " + JSON.stringify(err)));
  }
  
  private showNotification(song: Song) {
    let notificationRequest = {
      content: {
        contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
        normal: {
          title: "正在播放",
          text: song.title,
          additionalText: song.artist
        }
      },
      actionButtons: [
        {
          title: "上一首",
          actionType: notification.ActionType.BUTTON_ACTION
        },
        {
          title: this.currentState === PlayerState.PLAYING ? "暂停" : "播放",
          actionType: notification.ActionType.BUTTON_ACTION
        },
        {
          title: "下一首",
          actionType: notification.ActionType.BUTTON_ACTION
        }
      ],
      id: 1,
      slotType: notification.SlotType.MEDIA_PLAYBACK
    };
    
    notification.publish(notificationRequest).then(() => {
      console.info("Notification published");
    });
  }
  
  // 修改play方法
  async play(song: Song) {
    // ...之前代码
    this.registerBackgroundTask();
    this.showNotification(song);
  }
}

7. 实际项目注意事项

  1. ​性能优化​​:对于大量歌曲,使用LazyForEach优化列表性能
  2. ​网络请求​​:实现网络歌曲的缓存机制
  3. ​错误处理​​:完善网络错误、文件错误的处理
  4. ​权限管理​​:动态申请运行时权限
  5. ​国际化​​:支持多语言
  6. ​主题切换​​:实现日间/夜间模式
  7. ​播放模式​​:实现单曲循环、列表循环、随机播放等模式
  8. ​音效设置​​:添加均衡器、音效设置
  9. ​本地音乐扫描​​:实现设备本地音乐文件扫描功能
  10. ​播放历史​​:记录播放历史功能

通过以上实现,你可以在HarmonyOS 5应用中创建类似QQ音乐的全功能音乐播放器,包含播放控制、歌词显示、播放列表管理等核心功能,并支持后台播放和通知控制。