HarmonyOS音乐播放器开发实战:从零到一打造完整鸿蒙系统音乐播放器应用 1

HarmonyOS音乐播放器开发实战:从零到一打造完整应用

引言

随着HarmonyOS生态的不断发展,越来越多的开发者开始关注这一全新的操作系统。今天,我将分享一个完整的HarmonyOS音乐播放器应用的开发过程,涵盖从基础功能到高级特性的实现,希望能为有志于HarmonyOS开发的朋友们提供一些参考。

项目概述

这个音乐播放器应用支持本地音乐扫描、播放控制、歌词显示、主题切换等核心功能,采用了现代化的架构设计和用户界面,为用户提供流畅的音乐播放体验。

技术架构

核心技术栈

  • 开发语言: ArkTS
  • 开发框架: HarmonyOS UI Kit
  • 开发工具: DevEco Studio
  • 音频播放: AVPlayer
  • 数据存储: SQLite

架构设计

项目采用MVVM架构模式,将业务逻辑与UI层分离,提高代码的可维护性和可测试性:

plaintext

复制代码
`UI层 (Index.ets, PlayPage.ets) 
    ↓
ViewModel层 (MusicViewModel.ets)
    ↓
服务层 (AudioPlayer.ets, Database.ets)
    ↓
数据层 (SQLite数据库)`

核心功能实现

1. 音乐扫描与管理

音乐扫描是播放器的基础功能。我们实现了多种扫描方式:

复制代码

typescript

复制代码
`// 支持从系统媒体库扫描
async scanMediaLibrary(): Promise<void> {
  // 使用photoAccessHelper扫描音频文件
}

// 支持本地文件夹扫描
async scanLocalMusic(): Promise<void> {
  // 遍历指定目录下的音频文件
}

// 支持手动选择文件
async selectMusicFiles(): Promise<void> {
  // 使用AudioViewPicker选择音频文件
}`

2. 播放控制与状态管理

播放控制是音乐播放器的核心。我们使用单例模式的AudioPlayer来管理播放状态:

复制代码

typescript

复制代码
`// 播放器单例模式
class AudioPlayer {
  private static instance: AudioPlayer;
  static getInstance(context: UIAbilityContext): AudioPlayer {
    if (!AudioPlayer.instance) {
      AudioPlayer.instance = new AudioPlayer(context);
    }
    return AudioPlayer.instance;
  }
  
  // 播放控制方法
  async play(music: MusicItem): Promise<void> { /* ... */ }
  async pause(): Promise<void> { /* ... */ }
  async resume(): Promise<void> { /* ... */ }
  async seekTo(position: number): Promise<void> { /* ... */ }
}`

3. 歌词同步与显示

歌词功能是提升用户体验的重要特性。我们实现了LRC格式歌词的解析和同步显示:

复制代码

typescript

复制代码
`// LRC歌词解析
function parseLyrics(lyricsText: string): LyricLine[] {
  const lines = lyricsText.split('\n');
  const lyricLines: LyricLine[] = [];
  
  for (const line of lines) {
    const matches = line.match(/\[(\d{2}):(\d{2})\.(\d{2})\](.*)/);
    if (matches) {
      const minutes = parseInt(matches[1]);
      const seconds = parseInt(matches[2]);
      const milliseconds = parseInt(matches[3]);
      const time = minutes * 60000 + seconds * 1000 + milliseconds * 10;
      const text = matches[4].trim();
      
      lyricLines.push({ time, text });
    }
  }
  
  return lyricLines.sort((a, b) => a.time - b.time);
}`

4. 主题切换与UI适配

为了提供更好的视觉体验,我们实现了暗色/浅色主题切换功能:

复制代码

typescript

复制代码
`// 主题状态管理
@StorageLink('isDarkTheme') isDarkTheme: boolean = false;

// 根据主题动态设置颜色
.backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF')
.fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')`

关键技术点解析

1. 进度条优化

进度条是播放器的重要组件。我们实现了防抖动和动态交互效果:

复制代码

typescript

复制代码
`// 进度条组件
Slider({
  value: this.isDragging ? this.dragValue : this.currentPosition,
  min: 0,
  max: this.duration > 0 ? this.duration : 1,
  step: 1000,
  style: SliderStyle.OutSet
})
.height(this.isProgressHovered ? 22 : 18)  // 动态高度
.blockSize({ width: 24, height: 24 })      // 滑块尺寸
.blockColor('#FFFFFF')                      // 白色滑块
.trackColor('rgba(255,255,255,0.3)')       // 半透明白色轨道`

防抖动机制通过分离拖动状态和播放状态实现:

复制代码

typescript

复制代码
`@State isDragging: boolean = false;
@State dragValue: number = 0;

// 拖动时使用临时值,避免播放器更新冲突
value: this.isDragging ? this.dragValue : this.currentPosition`

2. 全局状态同步

为实现跨页面状态同步,我们使用AppStorage:

复制代码

typescript

复制代码
`// 全局状态管理
@StorageLink('currentMusicId') currentMusicId: number = 0;
@StorageLink('currentMusicTitle') currentMusicTitle: string = '';
@StorageLink('isPlaying') isPlaying: boolean = false;
@StorageLink('currentPosition') currentPosition: number = 0;`

3. 播放状态防抖处理

快速点击播放/暂停按钮容易导致状态混乱,我们实现了防抖机制:

复制代码

typescript

复制代码
`private isToggling: boolean = false;
private lastToggleTime: number = 0;
private TOGGLE_DEBOUNCE_MS: number = 300;

async togglePlayPause(): Promise<void> {
  const now = Date.now();
  
  if (this.isToggling || (now - this.lastToggleTime < this.TOGGLE_DEBOUNCE_MS)) {
    return; // 防抖处理
  }
  
  this.isToggling = true;
  this.lastToggleTime = now;
  
  try {
    // 基于播放器真实状态判断
    const realIsPlaying = this.audioPlayer.getIsPlaying();
    if (realIsPlaying) {
      await this.audioPlayer.pause();
    } else {
      await this.audioPlayer.resume();
    }
  } finally {
    setTimeout(() => {
      this.isToggling = false;
    }, this.TOGGLE_DEBOUNCE_MS);
  }
}`
bash 复制代码
MyApplication/
├── AppScope/
│   └── resources/
│       └── base/
│           ├── element/
│           │   └── string.json
│           └── media/
│               └── layered_image.json
├── entry/
│   ├── src/
│   │   └── main/
│   │       └── ets/
│   │           ├── entryability/
│   │           │   └── EntryAbility.ets
│   │           ├── entrybackupability/
│   │           │   └── EntryBackupAbility.ets
│   │           └── pages/
│   │               ├── components/
│   │               │   ├── MusicItem.ets
│   │               │   ├── PlayerBar.ets
│   │               │   └── PlaylistDrawer.ets
│   │               ├── models/
│   │               │   └── MusicItem.ets
│   │               ├── services/
│   │               │   ├── AudioPlayer.ets
│   │               │   └── Database.ets
│   │               ├── utils/
│   │               │   ├── FileUtils.ets
│   │               │   └── MusicService.ets
│   │               ├── viewmodels/
│   │               │   └── MusicViewModel.ets
│   │               ├── Index.ets
│   │               └── PlayPage.ets
│   ├── build-profile.json5
│   ├── module.json5
│   └── resources/
│       └── base/
│           ├── element/
│           │   ├── color.json
│           │   ├── float.json
│           │   └── string.json
│       └── profile/
│           └── main_pages.json
├── hvigor/
│   └── hvigor-config.json5
├── .gitignore
├── build-profile.json5
├── code-linter.json5
├── hvigorfile.ts
├── obfuscation-rules.txt
└── oh-package.json5
HarmonyOS音乐播放器项目文件结构及作用说明
项目概览
这是一个基于HarmonyOS NEXT和ArkTS开发的完整音乐播放器应用,采用模块化架构设计,具有良好的可维护性和扩展性。
详细文件作用说明
1. AppScope/ - 应用全局资源
resources/base/element/string.json: 存储应用的全局字符串资源,如应用名称、描述等
resources/base/media/layered_image.json: 定义应用的分层图像资源
2. entry/ - 应用主模块
2.1 src/main/ets/ - 源代码目录
entryability/EntryAbility.ets: 应用入口文件,定义应用生命周期和初始化逻辑
entrybackupability/EntryBackupAbility.ets: 应用备份能力实现
2.2 pages/ - 页面和组件模块
components/ - UI组件库
MusicItem.ets: 音乐列表项组件,显示单个音乐信息
PlayerBar.ets: 底部播放控制栏组件,包含进度条、播放控制按钮等
PlaylistDrawer.ets: 播放列表抽屉组件,显示当前播放队列
models/ - 数据模型
MusicItem.ets: 定义音乐数据模型,包含音乐的基本属性(标题、艺术家、时长等)
services/ - 核心服务
AudioPlayer.ets: 音频播放服务,封装AVPlayer功能,提供播放、暂停、跳转等控制接口
Database.ets: 数据库服务,使用SQLite管理音乐信息、收藏状态等数据
utils/ - 工具类
FileUtils.ets: 文件操作工具,支持歌词文件读取、多编码格式解析
MusicService.ets: 音乐服务工具,提供音乐扫描、处理等功能
viewmodels/ - 视图模型
MusicViewModel.ets: MVVM架构中的ViewModel层,管理业务逻辑和状态
Index.ets: 应用主页面,显示音乐列表、搜索功能等
PlayPage.ets: 播放页面,包含封面显示、歌词展示、播放控制等功能
2.3 resources/ - 应用资源
base/element/color.json: 定义应用颜色主题
base/element/float.json: 定义应用浮点数值资源
base/element/string.json: 应用字符串资源
profile/main_pages.json: 定义应用页面结构和路由
2.4 build-profile.json5: 模块构建配置文件
2.5 module.json5: 模块配置文件,定义模块的基本信息和能力
3. hvigor/ - 构建工具配置
hvigor-config.json5: hvigor构建工具的全局配置
4. 根目录配置文件
.gitignore: Git版本控制忽略文件配置
build-profile.json5: 项目级构建配置,定义编译SDK版本、构建模式等
code-linter.json5: 代码规范检查配置
hvigorfile.ts: hvigor构建脚本
obfuscation-rules.txt: 代码混淆规则
oh-package.json5: OpenHarmony包管理配置
架构特点
1. 模块化设计
通过清晰的目录结构分离不同功能模块
UI组件、业务逻辑、数据服务相互独立
2. MVVM架构
View层: Index.ets, PlayPage.ets
ViewModel层: MusicViewModel.ets
Model层: Database.ets, AudioPlayer.ets
3. 状态管理
使用AppStorage实现全局状态管理
通过@StorageLink实现组件间状态同步
4. 功能特性
音乐播放控制(播放、暂停、上一曲、下一曲)
播放进度控制(拖动进度条)
歌词同步显示
主题切换(暗色/浅色)
音乐扫描和管理
收藏功能
AVSession集成(通知栏控制)
bash 复制代码
// components/MusicItem.ets
import { MusicItem as MusicItemModel, formatTime } from '../models/MusicItem';

@Component
export struct MusicListItem {
  @Prop music: MusicItemModel;
  @Prop isCurrent: boolean = false;
  @Prop isPlaying: boolean = false;
  private onPlay: () => void = () => {};
  private onFavorite: () => void = () => {};

  build() {
    Row() {
      // 专辑封面
      Stack() {
        Image(this.music.cover)
          .width(50)
          .height(50)
          .borderRadius(8)
          .objectFit(ImageFit.Cover)

        if (this.isCurrent && this.isPlaying) {
          Row()
            .width(50)
            .height(50)
            .borderRadius(8)
            .backgroundColor('rgba(0,0,0,0.3)')

          Image($r('app.media.ic_playing'))
            .width(24)
            .height(24)
            .fillColor('#FFFFFF')
        }
      }
      .margin({ right: 12 })

      // 音乐信息
      Column() {
        Text(this.music.title)
          .fontSize(16)
          .fontWeight(this.isCurrent ? FontWeight.Bold : FontWeight.Normal)
          .fontColor(this.isCurrent ? '#FF5722' : '#333333')
          .textAlign(TextAlign.Start)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .width('100%')

        Row() {
          Text(`${this.music.artist}`)
            .fontSize(12)
            .fontColor(this.isCurrent ? '#FF8A65' : '#666666')
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          Text(` · ${formatTime(this.music.duration)}`)
            .fontSize(12)
            .fontColor('#999999')
        }
        .width('100%')
        .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      // 收藏按钮
      Image(this.music.isFavorite ? $r('app.media.ic_favorite') : $r('app.media.ic_favorite_border'))
        .width(24)
        .height(24)
        .fillColor(this.music.isFavorite ? '#FF5722' : '#CCCCCC')
        .margin({ right: 8 })
        .onClick(() => {
          this.onFavorite();
        })

      // 播放按钮
      Image(this.isCurrent && this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
        .width(32)
        .height(32)
        .fillColor(this.isCurrent ? '#FF5722' : '#666666')
        .onClick(() => {
          this.onPlay();
        })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
    .backgroundColor(this.isCurrent ? '#FFF8E1' : '#FFFFFF')
    .onClick(() => {
      this.onPlay();
    })
  }
}
bash 复制代码
// components/PlaylistDrawer.ets - 播放列表抽屉组件
import { MusicItem } from '../models/MusicItem';

@Component
export struct PlaylistDrawer {
  @Link showDrawer: boolean;
  @Prop playlist: MusicItem[];
  @Prop currentMusicId: number;
  onPlayMusic?: (music: MusicItem) => void;
  
  // 从 AppStorage 读取主题状态
  @StorageLink('isDarkTheme') isDarkTheme: boolean = false;

  build() {
    Column() {
      // 遮罩层
      Column()
        .width('100%')
        .layoutWeight(1)
        .backgroundColor(this.isDarkTheme ? 'rgba(15, 52, 96, 0.85)' : 'rgba(0, 0, 0, 0.5)')
        .onClick(() => {
          this.showDrawer = false;
        })

      // 播放列表内容
      Column() {
        // 标题栏
        Row() {
          Text('播放列表')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.isDarkTheme ? '#FFFFFF' : '#333333')

          Blank()

          Text(`${this.playlist.length}首`)
            .fontSize(14)
            .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')

          Image($r('app.media.ic_close'))
            .width(24)
            .height(24)
            .fillColor(this.isDarkTheme ? '#AAAAAA' : '#666666')
            .margin({ left: 16 })
            .onClick(() => {
              this.showDrawer = false;
            })
        }
        .width('100%')
        .height(56)
        .padding({ left: 16, right: 16 })

        // 列表
        List({ space: 1 }) {
          ForEach(this.playlist, (item: MusicItem, index: number) => {
            ListItem() {
              Row() {
                Text(`${index + 1}`)
                  .fontSize(14)
                  .fontColor(this.currentMusicId === item.id ? 
                    (this.isDarkTheme ? '#FF4158' : '#667EEA') : 
                    (this.isDarkTheme ? '#AAAAAA' : '#999999')
                  )
                  .width(30)

                Column() {
                  Text(item.title)
                    .fontSize(15)
                    .fontColor(this.currentMusicId === item.id ? 
                      (this.isDarkTheme ? '#FF4158' : '#667EEA') : 
                      (this.isDarkTheme ? '#FFFFFF' : '#333333')
                    )
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })

                  Text(item.artist)
                    .fontSize(12)
                    .fontColor(this.isDarkTheme ? '#AAAAAA' : '#999999')
                    .margin({ top: 2 })
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)

                if (this.currentMusicId === item.id) {
                  Image($r('app.media.ic_playing'))
                    .width(20)
                    .height(20)
                    .fillColor(this.isDarkTheme ? '#FF4158' : '#667EEA')
                }
              }
              .width('100%')
              .height(56)
              .padding({ left: 16, right: 16 })
              .backgroundColor(this.currentMusicId === item.id ? 
                (this.isDarkTheme ? 'rgba(255, 65, 88, 0.15)' : 'rgba(102, 126, 234, 0.1)') : 
                'transparent'
              )
              .onClick(() => {
                if (this.onPlayMusic) {
                  this.onPlayMusic(item);
                }
              })
            }
          }, (item: MusicItem) => item.id.toString())
        }
        .width('100%')
        .layoutWeight(1)
        .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.3)' : '#FFFFFF')
        .divider({
          strokeWidth: 0.5,
          color: this.isDarkTheme ? 'rgba(255, 255, 255, 0.1)' : '#F0F0F0',
          startMargin: 46,
          endMargin: 16
        })
      }
      .width('100%')
      .height('60%')
      .backgroundColor(this.isDarkTheme ? 'rgba(22, 33, 62, 0.95)' : '#FFFFFF')
      .borderRadius({ topLeft: 20, topRight: 20 })
      .shadow({
        radius: 12,
        color: this.isDarkTheme ? '#40000000' : '#20000000',
        offsetX: 0,
        offsetY: -3
      })
    }
    .width('100%')
    .height('100%')
  }
}
bash 复制代码
// models/MusicItem.ets

// 歌词行数据
export interface LyricLine {
  time: number; // 毫秒
  text: string;
}

// 播放模式枚举
export enum PlayMode {
  SEQUENCE = 0,    // 顺序播放
  LOOP = 1,        // 列表循环
  SINGLE = 2,      // 单曲循环
  RANDOM = 3       // 随机播放
}

// 音乐项数据模型
export interface MusicItem {
  id: number;
  title: string;
  artist: string;
  album: string;
  duration: number;
  filePath: string;
  cover: ResourceStr;
  fileName: string;
  fileSize: number;
  createTime: number;
  playCount: number;
  isFavorite: boolean;  // 是否收藏
  lyrics: string;       // 歌词内容(LRC格式)
}

// 播放列表
export interface Playlist {
  id: number;
  name: string;
  cover: ResourceStr;
  musicCount: number;
  createTime: number;
}

// 播放历史
export interface PlayHistory {
  id: number;
  musicId: number;
  playTime: number;
  position: number;
}

// 解析LRC歌词
export function parseLyrics(lrcContent: string): LyricLine[] {
  const lines = lrcContent.split('\n');
  const result: LyricLine[] = [];

  for (const line of lines) {
    // 匹配 [mm:ss.xx] 或 [mm:ss] 格式
    const match = line.match(/\[(\d{2}):(\d{2})([.:]\d{2,3})?\](.*)/);
    if (match) {
      const minutes = parseInt(match[1]);
      const seconds = parseInt(match[2]);
      const milliseconds = match[3] ? parseInt(match[3].slice(1)) : 0;
      const text = match[4].trim();

      if (text) {
        const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds;
        result.push({ time, text });
      }
    }
  }

  // 按时间排序
  result.sort((a, b) => a.time - b.time);
  return result;
}

// 格式化时间为 mm:ss
export function 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')}`;
}
bash 复制代码
// services/AudioPlayer.ets
import { media } from '@kit.MediaKit';
import { audio } from '@kit.AudioKit';
import { avSession } from '@kit.AVSessionKit';
import { common } from '@kit.AbilityKit';
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { MusicItem, PlayMode } from '../models/MusicItem';
import { BusinessError } from '@kit.BasicServicesKit';

const TAG = 'AudioPlayer';
const DOMAIN = 0x0001;

// 定义播放器状态接口
interface PlayerState {
  isPlaying: boolean;
  position: number;
  duration: number;
}

// 单例实例
let audioPlayerInstance: AudioPlayer | null = null;

export class AudioPlayer {
  private avPlayer: media.AVPlayer | null = null;
  private session: avSession.AVSession | null = null;
  private audioManager: audio.AudioManager | null = null;
  private context: common.UIAbilityContext;
  private currentMusic: MusicItem | null = null;
  private isPlaying: boolean = false;
  private currentPosition: number = 0;
  private playMode: PlayMode = PlayMode.LOOP;
  private playlist: MusicItem[] = [];
  private currentIndex: number = 0;
  private isInitialized: boolean = false;
  private currentFile: fs.File | null = null; // 保存当前文件句柄
  private lastProgressUpdate: number = 0; // 上次更新进度的时间戳
  private lastMetadataUpdate: string = ''; // 上次更新元数据的音乐ID(避免重复更新)
  private lastPlaybackStateUpdate: string = ''; // 上次更新的播放状态(避免重复更新)
  // 播放状态变化回调
  private onStateChange: (state: string) => void = () => {
  };
  private onPositionChange: (position: number) => void = () => {
  };
  private onMusicChange: (music: MusicItem) => void = () => {
  };
  private onPlayModeChange: (mode: PlayMode) => void = () => {
  };
  private onDurationChange: (duration: number) => void = () => {
  };
  private onFavoriteChange: (musicId: number, isFavorite: boolean) => void = () => {
  };

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  // 获取单例实例
  static getInstance(context: common.UIAbilityContext): AudioPlayer {
    if (!audioPlayerInstance) {
      hilog.info(DOMAIN, TAG, '创建新的单例实例');
      audioPlayerInstance = new AudioPlayer(context);
    }
    return audioPlayerInstance;
  }

  // 初始化播放器
  async init(): Promise<void> {
    // 如果已经初始化,直接返回
    if (this.isInitialized && this.avPlayer) {
      hilog.info(DOMAIN, TAG, '播放器已初始化,跳过');
      return;
    }

    hilog.info(DOMAIN, TAG, '===== 初始化播放器 =====');
    try {
      // 初始化音频管理器
      hilog.info(DOMAIN, TAG, '初始化音频管理器...');
      this.audioManager = audio.getAudioManager();
      hilog.info(DOMAIN, TAG, '音频管理器创建成功');

      hilog.info(DOMAIN, TAG, '调用 media.createAVPlayer()...');
      this.avPlayer = await media.createAVPlayer();
      hilog.info(DOMAIN, TAG, 'AVPlayer 创建成功');
      
      // 设置音频属性(关键:确保音频输出到蓝牙设备)
      this.avPlayer.audioInterruptMode = audio.InterruptMode.SHARE_MODE;
      hilog.info(DOMAIN, TAG, '音频中断模式已设置: SHARE_MODE');
      
      this.setEventCallBack();

      // 设置默认音量为 1.0(100%)
      await this.avPlayer.setVolume(1.0);
      hilog.info(DOMAIN, TAG, '默认音量已设置: 100%');

      // 初始化 AVSession 媒体会话
      await this.initAVSession();

      this.isInitialized = true;
      hilog.info(DOMAIN, TAG, '✅ 播放器初始化成功');
    } catch (error) {
      hilog.error(DOMAIN, TAG, '❌ 创建播放器失败: %{public}s', JSON.stringify(error));
      throw new Error(`创建播放器失败: ${JSON.stringify(error)}`);
    }
  }

  // 初始化 AVSession 媒体会话(通知栏控制)
  private async initAVSession(): Promise<void> {
    console.info('[AudioPlayer] ===== 初始化 AVSession =====');
    try {
      // 创建媒体会话
      this.session = await avSession.createAVSession(this.context, 'MusicPlayer', 'audio');
      console.info('[AudioPlayer] AVSession 创建成功');

      // 设置播放命令回调
      this.session.on('play', () => {
        console.info('[AudioPlayer] AVSession: 收到播放命令');
        this.resume();
      });

      this.session.on('pause', () => {
        console.info('[AudioPlayer] AVSession: 收到暂停命令');
        this.pause();
      });

      this.session.on('playNext', () => {
        console.info('[AudioPlayer] AVSession: 收到下一曲命令');
        this.playNext();
      });

      this.session.on('playPrevious', () => {
        console.info('[AudioPlayer] AVSession: 收到上一曲命令');
        this.playPrevious();
      });

      this.session.on('seek', (time: number) => {
        console.info(`[AudioPlayer] AVSession: 收到跳转命令 time=${time}`);
        this.seekTo(time);
      });

      this.session.on('stop', () => {
        console.info('[AudioPlayer] AVSession: 收到停止命令');
        this.stop();
      });

      // 激活会话
      await this.session.activate();
      console.info('[AudioPlayer] ✅ AVSession 初始化成功');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[AudioPlayer] ❌ AVSession 初始化失败: code=${err.code}, message=${err.message}`);
      // AVSession 失败不影响基本播放功能
    }
  }

  // 更新 AVSession 媒体信息
  private async updateSessionMetadata(): Promise<void> {
    if (!this.session || !this.currentMusic) {
      return;
    }

    try {
      // 避免重复更新相同音乐的元数据
      const musicKey = `${this.currentMusic.id}_${this.currentMusic.title}`;
      if (this.lastMetadataUpdate === musicKey) {
        hilog.info(DOMAIN, TAG, 'AVSession 元数据未变化,跳过更新');
        return;
      }

      const metadata: avSession.AVMetadata = {
        assetId: String(this.currentMusic.id),
        title: this.currentMusic.title || '未知歌曲',
        artist: this.currentMusic.artist || '未知歌手',
        album: this.currentMusic.album || '未知专辑',
        duration: this.currentMusic.duration || this.avPlayer?.duration || 0
      };

      // 注意:AVSession 的 mediaImage 只支持网络 URL,不支持本地文件路径
      // 本地封面路径不设置,避免 AVSession 警告
      // 如需显示封面,可将封面上传到服务器或使用 PixelMap

      await this.session.setAVMetadata(metadata);
      this.lastMetadataUpdate = musicKey;
      hilog.info(DOMAIN, TAG, 'AVSession 元数据已更新: %{public}s - %{public}s', 
        metadata.title, metadata.artist);
    } catch (error) {
      hilog.error(DOMAIN, TAG, '更新 AVSession 元数据失败: %{public}s', JSON.stringify(error));
    }
  }

  // 更新 AVSession 播放状态
  private async updateSessionPlaybackState(state: string): Promise<void> {
    if (!this.session) {
      return;
    }

    try {
      // 避免重复更新相同状态
      if (this.lastPlaybackStateUpdate === state) {
        return;
      }

      let playbackState: avSession.PlaybackState;

      switch (state) {
        case 'playing':
          playbackState = avSession.PlaybackState.PLAYBACK_STATE_PLAY;
          break;
        case 'paused':
          playbackState = avSession.PlaybackState.PLAYBACK_STATE_PAUSE;
          break;
        case 'completed':
        case 'stopped':
        case 'idle':
          playbackState = avSession.PlaybackState.PLAYBACK_STATE_STOP;
          break;
        default:
          // 其他状态不更新
          return;
      }

      const avPlaybackState: avSession.AVPlaybackState = {
        state: playbackState,
        position: {
          elapsedTime: this.currentPosition,
          updateTime: Date.now()
        },
        speed: 1.0,
        isFavorite: this.currentMusic?.isFavorite || false
      };

      await this.session.setAVPlaybackState(avPlaybackState);
      this.lastPlaybackStateUpdate = state;
      hilog.info(DOMAIN, TAG, 'AVSession 播放状态已更新: %{public}s', state);
    } catch (error) {
      hilog.error(DOMAIN, TAG, '更新 AVSession 播放状态失败: %{public}s', JSON.stringify(error));
    }
  }

  // 检查是否已初始化
  isReady(): boolean {
    return this.isInitialized && this.avPlayer !== null;
  }

  // 获取当前播放状态
  getIsPlaying(): boolean {
    return this.isPlaying;
  }

  // 设置事件回调
  private setEventCallBack(): void {
    if (!this.avPlayer) {
      hilog.error(DOMAIN, TAG, 'setEventCallBack: avPlayer 为 null');
      return;
    }

    hilog.info(DOMAIN, TAG, '设置事件回调...');

    this.avPlayer.on('stateChange', (state: string) => {
      hilog.info(DOMAIN, TAG, '>>>>>>> 状态变化: %{public}s <<<<<<<', state);
      switch (state) {
        case 'idle':
          hilog.info(DOMAIN, TAG, '  -> 播放器空闲 (idle)');
          this.isPlaying = false;
          this.updateGlobalState();
          break;
        case 'initialized':
          hilog.info(DOMAIN, TAG, '  -> 已初始化 (initialized),准备调用 prepare()...');
          if (this.avPlayer) {
            this.avPlayer.prepare().then(() => {
              hilog.info(DOMAIN, TAG, '  -> prepare() 调用成功');
            }).catch((err: BusinessError) => {
              hilog.error(DOMAIN, TAG, '  -> prepare() 失败: code=%{public}d, msg=%{public}s', err.code, err.message);
            });
          }
          break;
        case 'prepared':
          hilog.info(DOMAIN, TAG, '  -> 已准备 (prepared),准备调用 play()...');
          // 获取实际时长
          if (this.avPlayer && this.avPlayer.duration > 0) {
            hilog.info(DOMAIN, TAG, '  -> 音乐时长: %{public}d ms', this.avPlayer.duration);
            this.onDurationChange(this.avPlayer.duration);
          }
          if (this.avPlayer) {
            this.avPlayer.play().then(() => {
              hilog.info(DOMAIN, TAG, '  -> play() 调用成功');
            }).catch((err: BusinessError) => {
              hilog.error(DOMAIN, TAG, '  -> play() 失败: code=%{public}d, msg=%{public}s', err.code, err.message);
            });
          }
          break;
        case 'playing':
          hilog.info(DOMAIN, TAG, '  -> ✅ 正在播放 (playing)');
          this.isPlaying = true;
          this.updateGlobalState();
          break;
        case 'paused':
          hilog.info(DOMAIN, TAG, '  -> 已暂停 (paused)');
          this.isPlaying = false;
          this.updateGlobalState();
          break;
        case 'completed':
          hilog.info(DOMAIN, TAG, '  -> 播放完成 (completed)');
          this.isPlaying = false;
          this.currentPosition = 0;
          this.updateGlobalState();
          this.handlePlayComplete();
          break;
        case 'stopped':
          hilog.info(DOMAIN, TAG, '  -> 已停止 (stopped)');
          this.isPlaying = false;
          this.updateGlobalState();
          break;
        case 'released':
          hilog.info(DOMAIN, TAG, '  -> 已释放 (released)');
          this.isPlaying = false;
          this.updateGlobalState();
          break;
        case 'error':
          hilog.error(DOMAIN, TAG, '  -> ❌ 错误状态 (error)');
          this.isPlaying = false;
          this.updateGlobalState();
          break;
        default:
          hilog.info(DOMAIN, TAG, '  -> 未知状态: %{public}s', state);
          break;
      }
      this.onStateChange(state);
      // 更新 AVSession 播放状态
      this.updateSessionPlaybackState(state);
    });

    // 时间更新回调
    this.avPlayer.on('timeUpdate', (time: number) => {
      this.currentPosition = time;
      this.onPositionChange(time);
      // 更新全局进度(节流:每秒更新一次)
      const now = Date.now();
      if (!this.lastProgressUpdate || now - this.lastProgressUpdate > 1000) {
        AppStorage.setOrCreate('currentPosition', time);
        this.lastProgressUpdate = now;
      }
    });

    // 错误回调 - 详细输出
    this.avPlayer.on('error', (err: BusinessError) => {
      hilog.error(DOMAIN, TAG, '>>>>>>> 播放错误 <<<<<<<');
      hilog.error(DOMAIN, TAG, '  code: %{public}d', err.code);
      hilog.error(DOMAIN, TAG, '  message: %{public}s', err.message);
      hilog.error(DOMAIN, TAG, '  name: %{public}s', err.name);
      hilog.error(DOMAIN, TAG, '  当前音乐: %{public}s', this.currentMusic?.title || '无');
      hilog.error(DOMAIN, TAG, '  文件路径: %{public}s', this.currentMusic?.filePath || '无');
    });

    hilog.info(DOMAIN, TAG, '事件回调设置完成');
  }

  // 更新全局状态到 AppStorage
  private updateGlobalState(): void {
    AppStorage.setOrCreate('isPlaying', this.isPlaying);
    if (this.currentMusic) {
      AppStorage.setOrCreate('currentMusicId', this.currentMusic.id);
      AppStorage.setOrCreate('currentMusicTitle', this.currentMusic.title);
      AppStorage.setOrCreate('currentMusicArtist', this.currentMusic.artist);
      AppStorage.setOrCreate('currentMusicCover', this.currentMusic.cover);
      AppStorage.setOrCreate('currentMusicLyrics', this.currentMusic.lyrics || '');  // 添加歌词同步
    }
    AppStorage.setOrCreate('currentPosition', this.currentPosition);
    AppStorage.setOrCreate('playMode', this.playMode);
  }

  // 处理播放完成
  private handlePlayComplete(): void {
    switch (this.playMode) {
      case PlayMode.SINGLE:
        // 单曲循环
        if (this.currentMusic) {
          this.play(this.currentMusic);
        }
        break;
      case PlayMode.LOOP:
        // 列表循环
        this.playNext();
        break;
      case PlayMode.RANDOM:
        // 随机播放
        this.playRandom();
        break;
      case PlayMode.SEQUENCE:
        // 顺序播放
        if (this.currentIndex < this.playlist.length - 1) {
          this.playNext();
        }
        break;
      default:
        break;
    }
  }

  // 设置播放列表
  setPlaylist(list: MusicItem[]): void {
    this.playlist = list;
  }

  // 获取播放列表
  getPlaylist(): MusicItem[] {
    return this.playlist;
  }

  // 播放音乐
  async play(music: MusicItem): Promise<void> {
    hilog.info(DOMAIN, TAG, '======================================');
    hilog.info(DOMAIN, TAG, '===== 开始播放流程 =====');
    hilog.info(DOMAIN, TAG, '======================================');
    hilog.info(DOMAIN, TAG, '[1] 音乐信息:');
    hilog.info(DOMAIN, TAG, '    - 标题: %{public}s', music.title);
    hilog.info(DOMAIN, TAG, '    - 歌手: %{public}s', music.artist);
    hilog.info(DOMAIN, TAG, '    - 原始URI: %{public}s', music.filePath);
    hilog.info(DOMAIN, TAG, '    - ID: %{public}d', music.id);

    hilog.info(DOMAIN, TAG, '[2] 检查播放器状态:');
    hilog.info(DOMAIN, TAG, '    - avPlayer 存在: %{public}s', String(this.avPlayer !== null));
    hilog.info(DOMAIN, TAG, '    - isInitialized: %{public}s', String(this.isInitialized));

    if (!this.avPlayer) {
      hilog.info(DOMAIN, TAG, '[3] 播放器未初始化,开始初始化...');
      await this.init();
    }

    if (!this.avPlayer) {
      hilog.error(DOMAIN, TAG, '❌ 播放器初始化失败,无法播放');
      throw new Error('播放器初始化失败');
    }

    try {
      this.currentMusic = music;
      hilog.info(DOMAIN, TAG, '[4] 已设置 currentMusic');

      // 更新当前索引
      const index = this.playlist.findIndex(m => m.id === music.id);
      if (index !== -1) {
        this.currentIndex = index;
        hilog.info(DOMAIN, TAG, '[5] 当前索引: %{public}d / %{public}d', index, this.playlist.length);
      } else {
        hilog.info(DOMAIN, TAG, '[5] 音乐不在播放列表中');
      }

      const currentState = this.avPlayer.state;
      hilog.info(DOMAIN, TAG, '[6] 当前播放器状态: %{public}s', currentState);

      // 在设置新url前,必须将播放器重置到 idle 状态
      if (currentState !== 'idle') {
        hilog.info(DOMAIN, TAG, '[7] 重置播放器到 idle 状态...');
        try {
          await this.avPlayer.reset();
          hilog.info(DOMAIN, TAG, '[7] 重置成功,当前状态: %{public}s', this.avPlayer.state);
        } catch (resetError) {
          const err = resetError as BusinessError;
          hilog.error(DOMAIN, TAG, '[7] 重置失败: code=%{public}d, msg=%{public}s', err.code, err.message);
          hilog.info(DOMAIN, TAG, '[7] 尝试重新创建播放器...');
          await this.avPlayer.release();
          this.avPlayer = await media.createAVPlayer();
          this.setEventCallBack();
          hilog.info(DOMAIN, TAG, '[7] 播放器重新创建成功');
        }
      } else {
        hilog.info(DOMAIN, TAG, '[7] 播放器已在 idle 状态,无需重置');
      }

      // 将 URI 转换为 fd://
      hilog.info(DOMAIN, TAG, '[8] 转换 URI 为 fd://...');
      const fdUrl = await this.convertUriToFd(music.filePath);
      hilog.info(DOMAIN, TAG, '[8] 转换成功: %{public}s', fdUrl);

      // 设置播放源 - 使用 fd:// 格式
      hilog.info(DOMAIN, TAG, '[9] 设置播放源 URL...');
      this.avPlayer.url = fdUrl;
      hilog.info(DOMAIN, TAG, '[10] 播放源已设置,等待 stateChange -> initialized -> prepared -> playing');

      this.onMusicChange(music);
      hilog.info(DOMAIN, TAG, '[11] 已通知 onMusicChange');

      // 更新全局状态
      this.updateGlobalState();
      hilog.info(DOMAIN, TAG, '[12] 全局状态已更新');

      // 更新 AVSession 媒体信息
      await this.updateSessionMetadata();
      hilog.info(DOMAIN, TAG, '[13] AVSession 元数据已更新');
      hilog.info(DOMAIN, TAG, '===== 播放流程设置完成,等待回调 =====');
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN, TAG, '======================================');
      hilog.error(DOMAIN, TAG, '❌ 播放失败!');
      hilog.error(DOMAIN, TAG, '  code: %{public}d', err.code);
      hilog.error(DOMAIN, TAG, '  message: %{public}s', err.message);
      hilog.error(DOMAIN, TAG, '  完整错误: %{public}s', JSON.stringify(error));
      hilog.error(DOMAIN, TAG, '======================================');
      throw new Error(`播放失败: ${err.message}`);
    }
  }

  // 将 URI 转换为 fd:// 格式
  private async convertUriToFd(uri: string): Promise<string> {
    try {
      hilog.info(DOMAIN, TAG, '  正在打开文件: %{public}s', uri);
      
      // 关闭之前的文件句柄(如果有)
      if (this.currentFile !== null) {
        try {
          hilog.info(DOMAIN, TAG, '  关闭之前的文件句柄: %{public}d', this.currentFile.fd);
          fs.closeSync(this.currentFile);
        } catch (closeError) {
          hilog.warn(DOMAIN, TAG, '  关闭文件失败: %{public}s', JSON.stringify(closeError));
        }
        this.currentFile = null;
      }
      
      // 打开新文件并保存句柄
      this.currentFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
      const fd = this.currentFile.fd;
      hilog.info(DOMAIN, TAG, '  文件描述符: %{public}d', fd);
      hilog.info(DOMAIN, TAG, '  文件句柄已保存,将在播放期间保持打开');

      // 返回 fd:// 格式
      return `fd://${fd}`;
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN, TAG, '  ❌ 打开文件失败: code=%{public}d, msg=%{public}s', err.code, err.message);
      throw new Error(`打开文件失败: ${err.message}`);
    }
  }

  // 暂停播放
  async pause(): Promise<void> {
    console.info('[AudioPlayer] 暂停播放...');
    console.info(`[AudioPlayer]   当前状态: isPlaying=${this.isPlaying}, avPlayer.state=${this.avPlayer?.state}`);
    try {
      if (this.avPlayer && this.avPlayer.state === 'playing') {
        await this.avPlayer.pause();
        this.isPlaying = false;
        console.info('[AudioPlayer] ✅ 暂停成功');
      } else {
        console.warn('[AudioPlayer] 无法暂停: 播放器不在播放状态');
      }
    } catch (error) {
      console.error(`[AudioPlayer] ❌ 暂停失败: ${JSON.stringify(error)}`);
    }
  }

  // 恢复播放
  async resume(): Promise<void> {
    console.info('[AudioPlayer] 恢复播放...');
    console.info(`[AudioPlayer]   当前状态: isPlaying=${this.isPlaying}, avPlayer.state=${this.avPlayer?.state}`);
    try {
      if (this.avPlayer) {
        const state = this.avPlayer.state;
        if (state === 'paused') {
          // 从暂停状态恢复
          await this.avPlayer.play();
          this.isPlaying = true;
          console.info('[AudioPlayer] ✅ 恢复播放成功');
        } else if (state === 'prepared') {
          // 如果已准备但未播放,开始播放
          await this.avPlayer.play();
          this.isPlaying = true;
          console.info('[AudioPlayer] ✅ 开始播放(从 prepared 状态)');
        } else {
          // 其他状态无法恢复,需要重新播放
          console.warn(`[AudioPlayer] 无法恢复: 播放器状态=${state}`);
          if (this.currentMusic) {
            console.info('[AudioPlayer] 重新播放当前音乐');
            await this.play(this.currentMusic);
          }
        }
      } else {
        console.warn('[AudioPlayer] 无法恢复: 播放器未初始化');
      }
    } catch (error) {
      console.error(`[AudioPlayer] ❌ 恢复播放失败: ${JSON.stringify(error)}`);
      // 尝试重新播放
      if (this.currentMusic) {
        console.info('[AudioPlayer] 尝试重新播放');
        await this.play(this.currentMusic);
      }
    }
  }

  // 停止播放
  async stop(): Promise<void> {
    console.info('[AudioPlayer] 停止播放...');
    console.info(`[AudioPlayer]   当前状态: avPlayer.state=${this.avPlayer?.state}`);
    try {
      if (this.avPlayer) {
        const state = this.avPlayer.state;
        // 只有在特定状态下才能调用 stop()
        if (state === 'playing' || state === 'paused' || state === 'prepared') {
          console.info('[AudioPlayer] 调用 avPlayer.stop()...');
          await this.avPlayer.stop();
        }
        // reset 可以在大多数状态下调用
        if (state !== 'idle' && state !== 'released') {
          console.info('[AudioPlayer] 调用 avPlayer.reset()...');
          await this.avPlayer.reset();
        }
      }
      
      // 关闭文件句柄
      if (this.currentFile !== null) {
        try {
          console.info('[AudioPlayer] 关闭文件句柄: ' + this.currentFile.fd);
          fs.closeSync(this.currentFile);
          this.currentFile = null;
        } catch (closeError) {
          console.warn('[AudioPlayer] 关闭文件失败: ' + JSON.stringify(closeError));
        }
      }
      
      this.isPlaying = false;
      this.currentPosition = 0;
      console.info('[AudioPlayer] ✅ 停止成功');
    } catch (error) {
      console.error(`[AudioPlayer] ❌ 停止播放失败: ${JSON.stringify(error)}`);
    }
  }

  // 跳转到指定位置
  async seekTo(position: number): Promise<void> {
    try {
      if (this.avPlayer) {
        const state = this.avPlayer.state;
        // 只在允许的状态下执行 seek 操作
        const allowedStates = ['prepared', 'playing', 'paused', 'completed'];
        if (allowedStates.includes(state)) {
          await this.avPlayer.seek(position);
          this.currentPosition = position;
          hilog.info(DOMAIN, TAG, 'Seek 成功: position=%{public}d, state=%{public}s', position, state);
        } else {
          hilog.warn(DOMAIN, TAG, 'Seek 被忽略: 当前状态 %{public}s 不支持 seek 操作', state);
        }
      }
    } catch (error) {
      const err = error as BusinessError;
      hilog.error(DOMAIN, TAG, 'Seek 失败: code=%{public}d, msg=%{public}s', err.code, err.message);
    }
  }

  // 设置音量
  async setVolume(volume: number): Promise<void> {
    try {
      if (this.avPlayer) {
        await this.avPlayer.setVolume(volume);
      }
    } catch (error) {
      console.error(`设置音量失败: ${JSON.stringify(error)}`);
    }
  }

  // 播放下一首
  async playNext(): Promise<void> {
    if (this.playlist.length === 0) {
      return;
    }

    if (this.playMode === PlayMode.RANDOM) {
      await this.playRandom();
    } else {
      this.currentIndex = (this.currentIndex + 1) % this.playlist.length;
      const nextMusic = this.playlist[this.currentIndex];
      if (nextMusic) {
        await this.play(nextMusic);
      }
    }
  }

  // 播放上一首
  async playPrevious(): Promise<void> {
    if (this.playlist.length === 0) {
      return;
    }

    if (this.playMode === PlayMode.RANDOM) {
      await this.playRandom();
    } else {
      this.currentIndex = (this.currentIndex - 1 + this.playlist.length) % this.playlist.length;
      const prevMusic = this.playlist[this.currentIndex];
      if (prevMusic) {
        await this.play(prevMusic);
      }
    }
  }

  // 随机播放
  async playRandom(): Promise<void> {
    if (this.playlist.length === 0) {
      return;
    }

    let randomIndex = Math.floor(Math.random() * this.playlist.length);
    // 避免播放同一首
    if (this.playlist.length > 1 && randomIndex === this.currentIndex) {
      randomIndex = (randomIndex + 1) % this.playlist.length;
    }
    this.currentIndex = randomIndex;
    const randomMusic = this.playlist[this.currentIndex];
    if (randomMusic) {
      await this.play(randomMusic);
    }
  }

  // 切换播放模式
  togglePlayMode(): PlayMode {
    const modes = [PlayMode.SEQUENCE, PlayMode.LOOP, PlayMode.SINGLE, PlayMode.RANDOM];
    const currentModeIndex = modes.indexOf(this.playMode);
    this.playMode = modes[(currentModeIndex + 1) % modes.length];
    this.onPlayModeChange(this.playMode);
    return this.playMode;
  }

  // 设置播放模式
  setPlayMode(mode: PlayMode): void {
    this.playMode = mode;
    this.onPlayModeChange(mode);
  }

  // 获取当前播放模式
  getPlayMode(): PlayMode {
    return this.playMode;
  }

  // 获取当前状态
  getCurrentState(): PlayerState {
    return {
      isPlaying: this.isPlaying,
      position: this.currentPosition,
      duration: this.currentMusic?.duration || 0
    };
  }

  // 获取当前播放的音乐
  getCurrentMusic(): MusicItem | null {
    return this.currentMusic;
  }

  // 获取当前索引
  getCurrentIndex(): number {
    return this.currentIndex;
  }

  // 设置状态变化监听
  setOnStateChangeListener(callback: (state: string) => void): void {
    this.onStateChange = callback;
  }

  // 设置位置变化监听
  setOnPositionChangeListener(callback: (position: number) => void): void {
    this.onPositionChange = callback;
  }

  // 设置音乐变化监听
  setOnMusicChangeListener(callback: (music: MusicItem) => void): void {
    this.onMusicChange = callback;
  }

  // 设置播放模式变化监听
  setOnPlayModeChangeListener(callback: (mode: PlayMode) => void): void {
    this.onPlayModeChange = callback;
  }

  // 设置时长变化监听
  setOnDurationChangeListener(callback: (duration: number) => void): void {
    this.onDurationChange = callback;
  }

  // 设置收藏变化监听
  setOnFavoriteChangeListener(callback: (musicId: number, isFavorite: boolean) => void): void {
    this.onFavoriteChange = callback;
  }

  // 通知收藏状态变化(跨页面同步)
  notifyFavoriteChanged(musicId: number, isFavorite: boolean): void {
    console.info(`[AudioPlayer] 收藏状态变化: musicId=${musicId}, isFavorite=${isFavorite}`);
    // 更新当前音乐的收藏状态
    if (this.currentMusic && this.currentMusic.id === musicId) {
      this.currentMusic.isFavorite = isFavorite;
    }
    // 更新播放列表中的收藏状态
    const index = this.playlist.findIndex(m => m.id === musicId);
    if (index !== -1) {
      this.playlist[index].isFavorite = isFavorite;
    }
    // 通知监听器
    this.onFavoriteChange(musicId, isFavorite);
  }

  // 获取当前位置
  getCurrentPosition(): number {
    return this.currentPosition;
  }

  // 获取当前播放器状态
  getPlayerState(): string {
    return this.avPlayer?.state || 'idle';
  }

  // 获取实际时长
  getDuration(): number {
    return this.avPlayer?.duration || 0;
  }

  // 释放资源
  async release(): Promise<void> {
    try {
      await this.stop();
      if (this.avPlayer) {
        await this.avPlayer.release();
        this.avPlayer = null;
      }
      
      // 关闭文件句柄
      if (this.currentFile !== null) {
        try {
          console.info('[AudioPlayer] 释放时关闭文件句柄: ' + this.currentFile.fd);
          fs.closeSync(this.currentFile);
          this.currentFile = null;
        } catch (closeError) {
          console.warn('[AudioPlayer] 关闭文件失败: ' + JSON.stringify(closeError));
        }
      }
    } catch (error) {
      console.error(`释放资源失败: ${JSON.stringify(error)}`);
    }
  }
}
bash 复制代码
// services/Database.ets
import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { MusicItem, Playlist, PlayHistory } from '../models/MusicItem';
export class MusicDatabase {
  private rdbStore: relationalStore.RdbStore | null = null;
  private context: common.UIAbilityContext;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  // 初始化数据库
  async initDatabase(): Promise<void> {
    console.info('[Database] ===== 开始初始化数据库 =====');
    const config: relationalStore.StoreConfig = {
      name: 'MusicPlayer.db',
      securityLevel: relationalStore.SecurityLevel.S1
    };
    console.info('[Database] 数据库配置: name=MusicPlayer.db, securityLevel=S1');

    try {
      console.info('[Database] 调用 getRdbStore...');
      this.rdbStore = await relationalStore.getRdbStore(this.context, config);
      console.info('[Database] getRdbStore 成功');
      
      console.info('[Database] 创建数据表...');
      await this.createTables();
      console.info('[Database] ✅ 数据库初始化成功');
    } catch (error) {
      console.error(`[Database] ❌ 数据库初始化失败: ${error.message}`);
      throw new Error(`数据库初始化失败: ${error.message}`);
    }
    console.info('[Database] ===== 数据库初始化结束 =====');
  }

  // 创建数据表
  private async createTables(): Promise<void> {
    // 创建音乐表
    const createMusicTable = `
      CREATE TABLE IF NOT EXISTS music (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        artist TEXT,
        album TEXT,
        duration INTEGER,
        file_path TEXT UNIQUE NOT NULL,
        cover_url TEXT,
        file_name TEXT,
        file_size INTEGER,
        create_time INTEGER,
        play_count INTEGER DEFAULT 0,
        is_favorite INTEGER DEFAULT 0,
        lyrics TEXT
      )
    `;

    // 创建播放列表表
    const createPlaylistTable = `
      CREATE TABLE IF NOT EXISTS playlist (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        cover_url TEXT,
        music_count INTEGER DEFAULT 0,
        create_time INTEGER
      )
    `;

    // 创建播放历史表
    const createPlayHistoryTable = `
      CREATE TABLE IF NOT EXISTS play_history (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        music_id INTEGER,
        play_time INTEGER,
        position INTEGER,
        FOREIGN KEY (music_id) REFERENCES music(id)
      )
    `;

    // 创建播放列表-音乐关联表
    const createPlaylistMusicTable = `
      CREATE TABLE IF NOT EXISTS playlist_music (
        playlist_id INTEGER,
        music_id INTEGER,
        add_time INTEGER,
        PRIMARY KEY (playlist_id, music_id),
        FOREIGN KEY (playlist_id) REFERENCES playlist(id),
        FOREIGN KEY (music_id) REFERENCES music(id)
      )
    `;

    if (!this.rdbStore) {
      return;
    }
    try {
      await this.rdbStore.executeSql(createMusicTable);
      await this.rdbStore.executeSql(createPlaylistTable);
      await this.rdbStore.executeSql(createPlayHistoryTable);
      await this.rdbStore.executeSql(createPlaylistMusicTable);
    } catch (error) {
      console.error(`创建数据表失败: ${JSON.stringify(error)}`);
    }
  }

  // 添加音乐
  async addMusic(music: MusicItem): Promise<number> {
    console.info(`[Database] 添加音乐: "${music.title}" - ${music.artist}`);
    console.info(`[Database]   文件路径: ${music.filePath}`);
    const valueBucket: relationalStore.ValuesBucket = {
      'title': music.title,
      'artist': music.artist || '未知艺术家',
      'album': music.album || '未知专辑',
      'duration': music.duration,
      'file_path': music.filePath,
      'cover_url': '',
      'file_name': music.fileName,
      'file_size': music.fileSize,
      'create_time': Date.now(),
      'is_favorite': music.isFavorite ? 1 : 0,
      'lyrics': music.lyrics || ''
    };

    if (!this.rdbStore) {
      console.error('[Database] ❌ 数据库未初始化');
      throw new Error("数据库未初始化");
    }
    try {
      const insertId = await this.rdbStore.insert('music', valueBucket);
      console.info(`[Database] ✅ 添加成功, ID=${insertId}`);
      return insertId;
    } catch (error) {
      console.error(`[Database] ❌ 添加音乐失败: ${JSON.stringify(error)}`);
      throw new Error(`添加音乐失败: ${JSON.stringify(error)}`);
    }
  }

  // 获取所有音乐
  async getAllMusic(): Promise<MusicItem[]> {
    console.info('[Database] 获取所有音乐...');
    try {
      if (!this.rdbStore) {
        console.warn('[Database] 数据库未初始化,返回空列表');
        return [];
      }
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.orderByDesc('create_time');

      const result = await this.rdbStore.query(predicates, [
        'id', 'title', 'artist', 'album', 'duration',
        'file_path', 'cover_url', 'file_name', 'file_size',
        'create_time', 'play_count', 'is_favorite', 'lyrics'
      ]);

      const musicList: MusicItem[] = [];
      while (result.goToNextRow()) {
        musicList.push({
          id: result.getLong(result.getColumnIndex('id')),
          title: result.getString(result.getColumnIndex('title')),
          artist: result.getString(result.getColumnIndex('artist')),
          album: result.getString(result.getColumnIndex('album')),
          duration: result.getLong(result.getColumnIndex('duration')),
          filePath: result.getString(result.getColumnIndex('file_path')),
          cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
          fileName: result.getString(result.getColumnIndex('file_name')),
          fileSize: result.getLong(result.getColumnIndex('file_size')),
          createTime: result.getLong(result.getColumnIndex('create_time')),
          playCount: result.getLong(result.getColumnIndex('play_count')),
          isFavorite: result.getLong(result.getColumnIndex('is_favorite')) === 1,
          lyrics: result.getString(result.getColumnIndex('lyrics')) || ''
        });
      }
      result.close();
      console.info(`[Database] 查询完成,共 ${musicList.length} 首音乐`);
      return musicList;
    } catch (error) {
      console.error(`[Database] ❌ 获取音乐列表失败: ${JSON.stringify(error)}`);
      return [];
    }
  }

  // 根据 ID 获取单首音乐(包含最新歌词)
  async getMusicById(musicId: number): Promise<MusicItem | null> {
    console.info(`[Database] 获取音乐: id=${musicId}`);
    try {
      if (!this.rdbStore) {
        console.warn('[Database] 数据库未初始化');
        return null;
      }
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.equalTo('id', musicId);

      const result = await this.rdbStore.query(predicates, [
        'id', 'title', 'artist', 'album', 'duration',
        'file_path', 'cover_url', 'file_name', 'file_size',
        'create_time', 'play_count', 'is_favorite', 'lyrics'
      ]);

      if (result.goToNextRow()) {
        const music: MusicItem = {
          id: result.getLong(result.getColumnIndex('id')),
          title: result.getString(result.getColumnIndex('title')),
          artist: result.getString(result.getColumnIndex('artist')),
          album: result.getString(result.getColumnIndex('album')),
          duration: result.getLong(result.getColumnIndex('duration')),
          filePath: result.getString(result.getColumnIndex('file_path')),
          cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
          fileName: result.getString(result.getColumnIndex('file_name')),
          fileSize: result.getLong(result.getColumnIndex('file_size')),
          createTime: result.getLong(result.getColumnIndex('create_time')),
          playCount: result.getLong(result.getColumnIndex('play_count')),
          isFavorite: result.getLong(result.getColumnIndex('is_favorite')) === 1,
          lyrics: result.getString(result.getColumnIndex('lyrics')) || ''
        };
        result.close();
        console.info(`[Database] 找到音乐: ${music.title}, 歌词长度: ${music.lyrics.length}`);
        return music;
      }
      result.close();
      console.warn(`[Database] 未找到音乐: id=${musicId}`);
      return null;
    } catch (error) {
      console.error(`[Database] ❌ 获取音乐失败: ${JSON.stringify(error)}`);
      return null;
    }
  }

  // 搜索音乐
  async searchMusic(keyword: string): Promise<MusicItem[]> {
    try {
      if (!this.rdbStore) {
        return [];
      }
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.contains('title', keyword).or().contains('artist', keyword).or().contains('album', keyword);

      const result = await this.rdbStore.query(predicates, [
        'id', 'title', 'artist', 'album', 'duration',
        'file_path', 'cover_url', 'file_name', 'file_size',
        'create_time', 'play_count', 'is_favorite', 'lyrics'
      ]);

      const musicList: MusicItem[] = [];
      while (result.goToNextRow()) {
        musicList.push({
          id: result.getLong(result.getColumnIndex('id')),
          title: result.getString(result.getColumnIndex('title')),
          artist: result.getString(result.getColumnIndex('artist')),
          album: result.getString(result.getColumnIndex('album')),
          duration: result.getLong(result.getColumnIndex('duration')),
          filePath: result.getString(result.getColumnIndex('file_path')),
          cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
          fileName: result.getString(result.getColumnIndex('file_name')),
          fileSize: result.getLong(result.getColumnIndex('file_size')),
          createTime: result.getLong(result.getColumnIndex('create_time')),
          playCount: result.getLong(result.getColumnIndex('play_count')),
          isFavorite: result.getLong(result.getColumnIndex('is_favorite')) === 1,
          lyrics: result.getString(result.getColumnIndex('lyrics')) || ''
        });
      }
      result.close();
      return musicList;
    } catch (error) {
      console.error(`搜索音乐失败: ${JSON.stringify(error)}`);
      return [];
    }
  }

  // 切换收藏状态
  async toggleFavorite(musicId: number): Promise<boolean> {
    try {
      // 先获取当前状态
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.equalTo('id', musicId);
      if (!this.rdbStore) {
        return false;
      }
      const result = await this.rdbStore.query(predicates, ['is_favorite']);

      if (result.goToNextRow()) {
        const currentFavorite = result.getLong(result.getColumnIndex('is_favorite')) === 1;
        result.close();

        // 更新状态
        const updatePredicates = new relationalStore.RdbPredicates('music');
        updatePredicates.equalTo('id', musicId);
        const valueBucket: relationalStore.ValuesBucket = {
          'is_favorite': currentFavorite ? 0 : 1
        };
        await this.rdbStore.update(valueBucket, updatePredicates);
        return !currentFavorite;
      }
      result.close();
      return false;
    } catch (error) {
      console.error(`切换收藏状态失败: ${JSON.stringify(error)}`);
      return false;
    }
  }

  // 获取收藏的音乐
  async getFavoriteMusic(): Promise<MusicItem[]> {
    try {
      if (!this.rdbStore) {
        return [];
      }
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.equalTo('is_favorite', 1).orderByDesc('create_time');

      const result = await this.rdbStore.query(predicates, [
        'id', 'title', 'artist', 'album', 'duration',
        'file_path', 'cover_url', 'file_name', 'file_size',
        'create_time', 'play_count', 'is_favorite', 'lyrics'
      ]);
      
      const musicList: MusicItem[] = [];
      while (result.goToNextRow()) {
        musicList.push({
          id: result.getLong(result.getColumnIndex('id')),
          title: result.getString(result.getColumnIndex('title')),
          artist: result.getString(result.getColumnIndex('artist')),
          album: result.getString(result.getColumnIndex('album')),
          duration: result.getLong(result.getColumnIndex('duration')),
          filePath: result.getString(result.getColumnIndex('file_path')),
          cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
          fileName: result.getString(result.getColumnIndex('file_name')),
          fileSize: result.getLong(result.getColumnIndex('file_size')),
          createTime: result.getLong(result.getColumnIndex('create_time')),
          playCount: result.getLong(result.getColumnIndex('play_count')),
          isFavorite: true,
          lyrics: result.getString(result.getColumnIndex('lyrics')) || ''
        });
      }
      result.close();
      return musicList;
    } catch (error) {
      console.error(`获取收藏音乐失败: ${JSON.stringify(error)}`);
      return [];
    }
  }

  // 更新播放次数
  async updatePlayCount(musicId: number): Promise<void> {
    try {
      // 先获取当前播放次数
      const queryPredicates = new relationalStore.RdbPredicates('music');
      queryPredicates.equalTo('id', musicId);
      if (!this.rdbStore) {
        return;
      }
      const result = await this.rdbStore.query(queryPredicates, ['play_count']);

      if (result.goToNextRow()) {
        const currentCount = result.getLong(result.getColumnIndex('play_count'));
        result.close();

        const updatePredicates = new relationalStore.RdbPredicates('music');
        updatePredicates.equalTo('id', musicId);
        const valueBucket: relationalStore.ValuesBucket = {
          'play_count': currentCount + 1
        };
        await this.rdbStore.update(valueBucket, updatePredicates);
      } else {
        result.close();
      }
    } catch (error) {
      console.error(`更新播放次数失败: ${JSON.stringify(error)}`);
    }
  }
  // 添加播放历史
  async addPlayHistory(musicId: number, position: number): Promise<void> {
    const valueBucket: relationalStore.ValuesBucket = {
      'music_id': musicId,
      'play_time': Date.now(),
      'position': position
    };

    if (!this.rdbStore) {
      return;
    }
    try {
      await this.rdbStore.insert('play_history', valueBucket);
    } catch (error) {
      console.error(`添加播放历史失败: ${error.message}`);
    }
  }

  // 创建播放列表
  async createPlaylist(name: string): Promise<number> {
    const valueBucket: relationalStore.ValuesBucket = {
      'name': name,
      'cover_url': '',
      'create_time': Date.now()
    };

    if (!this.rdbStore) {
      throw new Error("数据库未初始化");
    }
    try {
      return await this.rdbStore.insert('playlist', valueBucket);
    } catch (error) {
      console.error(`创建播放列表失败: ${error.message}`);
      throw new Error(`创建播放列表失败: ${JSON.stringify(error)}`);
    }
  }

  // 获取所有播放列表
  async getAllPlaylists(): Promise<Playlist[]> {
    try {
      if (!this.rdbStore) {
        return [];
      }
      const predicates = new relationalStore.RdbPredicates('playlist');
      predicates.orderByDesc('create_time');

      const result = await this.rdbStore.query(predicates, [
        'id', 'name', 'cover_url', 'music_count', 'create_time'
      ]);

      const playlists: Playlist[] = [];
      while (result.goToNextRow()) {
        playlists.push({
          id: result.getLong(result.getColumnIndex('id')),
          name: result.getString(result.getColumnIndex('name')),
          cover: result.getString(result.getColumnIndex('cover_url')) || $r('app.media.ic_album'),
          musicCount: result.getLong(result.getColumnIndex('music_count')),
          createTime: result.getLong(result.getColumnIndex('create_time'))
        });
      }
      result.close();
      return playlists;
    } catch (error) {
      console.error(`获取播放列表失败: ${JSON.stringify(error)}`);
      return [];
    }
  }

  // 删除音乐
  async deleteMusic(musicId: number): Promise<boolean> {
    if (!this.rdbStore) {
      return false;
    }
    try {
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.equalTo('id', musicId);
      const deletedRows = await this.rdbStore.delete(predicates);
      return deletedRows > 0;
    } catch (error) {
      console.error(`删除音乐失败: ${JSON.stringify(error)}`);
      return false;
    }
  }

  // 清空所有音乐
  async clearAllMusic(): Promise<boolean> {
    console.info('[Database] 清空所有音乐...');
    if (!this.rdbStore) {
      console.error('[Database] ❌ 数据库未初始化');
      return false;
    }
    try {
      const predicates = new relationalStore.RdbPredicates('music');
      const deletedRows = await this.rdbStore.delete(predicates);
      console.info(`[Database] ✅ 清空成功,删除 ${deletedRows} 条记录`);
      return true;
    } catch (error) {
      console.error(`[Database] ❌ 清空音乐失败: ${JSON.stringify(error)}`);
      return false;
    }
  }

  // 检查音乐是否已存在(根据文件路径)
  async isMusicExists(filePath: string): Promise<boolean> {
    if (!this.rdbStore) {
      console.warn('[Database] 数据库未初始化,返回 false');
      return false;
    }
    try {
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.equalTo('file_path', filePath);
      const result = await this.rdbStore.query(predicates, ['id']);
      const exists = result.goToNextRow();
      result.close();
      if (exists) {
        console.info(`[Database] 音乐已存在: ${filePath}`);
      }
      return exists;
    } catch (error) {
      console.error(`[Database] ❌ 检查音乐存在失败: ${JSON.stringify(error)}`);
      return false;
    }
  }

  // 更新音乐时长
  async updateMusicDuration(musicId: number, duration: number): Promise<void> {
    if (!this.rdbStore) {
      return;
    }
    try {
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.equalTo('id', musicId);
      const valueBucket: relationalStore.ValuesBucket = {
        'duration': duration
      };
      await this.rdbStore.update(valueBucket, predicates);
    } catch (error) {
      console.error(`更新音乐时长失败: ${JSON.stringify(error)}`);
    }
  }

  // 更新音乐歌词
  async updateMusicLyrics(musicId: number, lyrics: string): Promise<void> {
    if (!this.rdbStore) {
      return;
    }
    try {
      const predicates = new relationalStore.RdbPredicates('music');
      predicates.equalTo('id', musicId);
      const valueBucket: relationalStore.ValuesBucket = {
        'lyrics': lyrics
      };
      await this.rdbStore.update(valueBucket, predicates);
      console.info(`[Database] 歌词已更新: musicId=${musicId}`);
    } catch (error) {
      console.error(`[Database] 更新歌词失败: ${JSON.stringify(error)}`);
    }
  }
}

没办法贴了,下一篇继续

相关推荐
yenggd3 小时前
华为批量下发配置命令使用telnetlib模块
网络·python·华为
个案命题4 小时前
鸿蒙ArkUI状态管理新宠:@Once装饰器全方位解析与实战
华为·harmonyos
云_杰4 小时前
取件伙伴性能提升——长列表
性能优化·harmonyos
花开彼岸天~5 小时前
Flutter跨平台开发鸿蒙化日志测试组件使用指南
flutter·elasticsearch·harmonyos
妮妮分享5 小时前
Harmony NEXT 定位 SDK:开启鸿蒙原生应用精准定位新时代
华为·harmonyos
w139548564227 小时前
在鸿蒙平台使用 sqlite3.dart 插件
华为·sqlite·harmonyos
7 小时前
鸿蒙——布局——相对布局
华为·harmonyos·
2501_946230987 小时前
Cordova&OpenHarmony维修记录管理指南
安全·harmonyos
w139548564229 小时前
在鸿蒙平台使用 sqlite3 插件
华为·sqlite·harmonyos