下面我将详细介绍如何使用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. 实际项目注意事项
- 性能优化:对于大量歌曲,使用LazyForEach优化列表性能
- 网络请求:实现网络歌曲的缓存机制
- 错误处理:完善网络错误、文件错误的处理
- 权限管理:动态申请运行时权限
- 国际化:支持多语言
- 主题切换:实现日间/夜间模式
- 播放模式:实现单曲循环、列表循环、随机播放等模式
- 音效设置:添加均衡器、音效设置
- 本地音乐扫描:实现设备本地音乐文件扫描功能
- 播放历史:记录播放历史功能
通过以上实现,你可以在HarmonyOS 5应用中创建类似QQ音乐的全功能音乐播放器,包含播放控制、歌词显示、播放列表管理等核心功能,并支持后台播放和通知控制。