HarmonyOS 音乐播放器开发教程——基于AVPlayer

作者:递归侠学算法

简介:热衷于鸿蒙开发,并致力于分享原创、优质且开源的鸿蒙项目。

一、概述

AVPlayer是鸿蒙OS中提供的多媒体播放API,支持播放音频和视频媒体源。本教程将详细介绍如何使用AVPlayer开发一个基础的音乐播放器应用,包括播放控制、状态监听、音量调节等功能。

二、环境准备

  • DevEco Studio 4.0或以上版本
  • 鸿蒙OS API 11或更高版本设备或模拟器
  • 基本的ArkTS编程知识

三、创建项目

首先,我们需要创建一个新的鸿蒙OS应用项目。通过DevEco Studio创建项目,选择"Empty Ability"模板,设置相应的应用信息。

四、配置权限

在项目的module.json5文件中配置网络访问和媒体访问权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      },
      {
        "name": "ohos.permission.READ_MEDIA"
      }
    ]
  }
}

五、引入AVPlayer相关模块

在页面中引入必要的模块:

import media from '@ohos.multimedia.media'
import { promptAction } from '@kit.ArkUI'

六、音乐播放器页面开发

1. 页面布局设计

创建一个简单的音乐播放器界面,包含音乐信息显示、播放控制按钮等元素:

@Entry
@Component
struct MusicPlayer {
  // 播放器控制器
  private player: media.AVPlayer = null
  // 播放状态
  @State isPlaying: boolean = false
  // 当前播放位置(毫秒)
  @State currentPosition: number = 0
  // 音乐总时长(毫秒)
  @State duration: number = 0
  // 音乐标题
  @State title: string = '未知歌曲'
  // 音量
  @State volume: number = 0.5
  // 计时器ID
  private timerId: number = -1

  build() {
    Column({ space: 20 }) {
      // 音乐信息显示区域
      Column() {
        Text(this.title)
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 20 })

        // 进度条
        Row() {
          Text(this.formatTime(this.currentPosition))
            .fontSize(14)
          Slider({
            value: this.currentPosition,
            min: 0,
            max: this.duration > 0 ? this.duration : 100,
            step: 1
          })
            .width('70%')
            .onChange((value: number) => {
              this.seekToPosition(value)
            })
          Text(this.formatTime(this.duration))
            .fontSize(14)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
      }
      .width('90%')
      .padding(20)
      .backgroundColor('#f5f5f5')
      .borderRadius(10)

      // 播放控制区域
      Row({ space: 20 }) {
        Button('上一曲')
          .width(100)
        Button(this.isPlaying ? '暂停' : '播放')
          .width(100)
          .onClick(() => {
            if (this.isPlaying) {
              this.pauseMusic()
            } else {
              this.playMusic()
            }
          })
        Button('下一曲')
          .width(100)
      }
      .justifyContent(FlexAlign.Center)

      // 音量控制
      Row() {
        Text('音量:')
          .fontSize(16)
        Slider({
          value: this.volume * 100,
          min: 0,
          max: 100,
          step: 1
        })
          .width('70%')
          .onChange((value: number) => {
            this.setVolume(value / 100)
          })
        Text(`${Math.floor(this.volume * 100)}%`)
          .fontSize(16)
      }
      .width('90%')
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#ffffff')
  }

  // 页面显示时调用
  aboutToAppear() {
    this.initializePlayer()
  }

  // 页面销毁时调用
  aboutToDisappear() {
    this.releasePlayer()
  }

  // 初始化播放器
  async initializePlayer() {
    try {
      // 创建AVPlayer实例
      this.player = await media.createAVPlayer()
      
      // 设置状态变化监听
      this.player.on('stateChange', (state: media.AVPlayerState) => {
        this.handleStateChange(state)
      })
      
      // 设置错误监听
      this.player.on('error', (error) => {
        promptAction.showToast({
          message: `播放错误: ${error.message}`,
          duration: 3000
        })
      })
      
      // 设置网络音频源
      this.player.url = 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/11.mp3'
      this.title = '示例音乐'
      
      // 设置音量
      this.setVolume(this.volume)
      
    } catch (error) {
      promptAction.showToast({
        message: `初始化播放器失败: ${error.message}`,
        duration: 3000
      })
    }
  }

  // 处理播放器状态变化
  handleStateChange(state: media.AVPlayerState) {
    console.log(`播放器状态变化: ${state}`)
    
    switch (state) {
      case 'initialized':
        // 初始化完成后准备播放
        this.player.prepare()
        break
      case 'prepared':
        // 准备完成后获取时长
        this.player.getDuration((err, durationValue) => {
          if (!err) {
            this.duration = durationValue
          }
        })
        break
      case 'playing':
        this.isPlaying = true
        // 开始定时获取播放位置
        this.startPositionTimer()
        break
      case 'paused':
        this.isPlaying = false
        // 暂停时停止定时器
        this.stopPositionTimer()
        break
      case 'completed':
        this.isPlaying = false
        this.currentPosition = this.duration
        this.stopPositionTimer()
        break
      case 'stopped':
        this.isPlaying = false
        this.currentPosition = 0
        this.stopPositionTimer()
        break
      case 'error':
        this.isPlaying = false
        this.stopPositionTimer()
        break
    }
  }

  // 开始播放音乐
  playMusic() {
    if (this.player) {
      this.player.play()
    }
  }

  // 暂停播放
  pauseMusic() {
    if (this.player) {
      this.player.pause()
    }
  }

  // 跳转到指定位置
  seekToPosition(position: number) {
    if (this.player) {
      this.player.seek(position, 'closest')
    }
  }

  // 设置音量
  setVolume(volume: number) {
    if (this.player) {
      this.volume = volume
      this.player.setVolume(volume, volume)
    }
  }

  // 释放播放器资源
  releasePlayer() {
    this.stopPositionTimer()
    if (this.player) {
      this.player.off('stateChange')
      this.player.off('error')
      this.player.stop()
      this.player.release()
      this.player = null
    }
  }

  // 开始定时获取播放位置
  startPositionTimer() {
    if (this.timerId !== -1) {
      clearInterval(this.timerId)
    }
    
    this.timerId = setInterval(() => {
      if (this.player) {
        this.player.getCurrentTime((err, currentTime) => {
          if (!err) {
            this.currentPosition = currentTime
          }
        })
      }
    }, 1000)
  }

  // 停止定时器
  stopPositionTimer() {
    if (this.timerId !== -1) {
      clearInterval(this.timerId)
      this.timerId = -1
    }
  }

  // 格式化时间,将毫秒转为分:秒格式
  formatTime(ms: number): string {
    if (isNaN(ms) || ms < 0) return '00:00'
    
    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')}`
  }
}

七、AVPlayer的生命周期管理

AVPlayer的状态流转如下:

初始化 -> 设置源 -> 准备 -> 播放/暂停/停止 -> 释放
  • 创建AVPlayer实例:media.createAVPlayer()
  • 设置媒体源:player.url = '音频URL'
  • 准备播放:player.prepare()
  • 开始播放:player.play()
  • 暂停播放:player.pause()
  • 停止播放:player.stop()
  • 释放资源:player.release()

八、高级特性

1. 播放倍速控制

// 设置播放速度
setPlaybackSpeed(speed: media.AVPlaybackSpeed) {
  if (this.player) {
    this.player.setPlaybackSpeed(speed)
  }
}

2. 循环播放

// 设置循环播放
setLoop(loop: boolean) {
  if (this.player) {
    this.player.setLooping(loop)
  }
}

3. 处理播放列表

创建一个音乐列表管理器:

class MusicListManager {
  musicList: Array<{
    id: number,
    title: string,
    url: string
  }> = []
  currentIndex: number = -1

  constructor(list: Array<{id: number, title: string, url: string}>) {
    this.musicList = list
    this.currentIndex = 0
  }

  getCurrentMusic() {
    if (this.currentIndex >= 0 && this.currentIndex < this.musicList.length) {
      return this.musicList[this.currentIndex]
    }
    return null
  }

  nextMusic() {
    this.currentIndex = (this.currentIndex + 1) % this.musicList.length
    return this.getCurrentMusic()
  }

  previousMusic() {
    this.currentIndex = (this.currentIndex - 1 + this.musicList.length) % this.musicList.length
    return this.getCurrentMusic()
  }
}

九、处理播放错误与异常

AVPlayer在播放过程中可能会遇到各种错误,比如网络问题、解码问题等。我们需要监听错误事件并进行处理:

player.on('error', (error) => {
  console.error(`播放错误: ${error.code}, ${error.message}`)
  promptAction.showToast({
    message: `播放失败: ${error.message}`,
    duration: 3000
  })
  
  // 根据错误类型进行处理
  switch (error.code) {
    case 1: // 内存不足
      // 释放资源后重试
      break
    case 4: // IO错误
      // 可能是网络问题,检查网络连接
      break
    // 其他错误处理...
  }
})

十、完整实例

下面是一个完整的基于AVPlayer的音乐播放器实现:

import media from '@ohos.multimedia.media'
import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct MusicPlayerApp {
  // 播放器实例
  private player: media.AVPlayer = null
  
  // 音乐列表
  private musicList: Array<{
    id: number,
    title: string, 
    url: string
  }> = [
    {
      id: 1,
      title: '示例音乐1',
      url: 'http://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/HeimaCloudMusic/11.mp3'
    },
    {
      id: 2,
      title: '示例音乐2',
      url: 'http://music.example.com/sample2.mp3'
    }
  ]
  
  // 当前播放的音乐索引
  @State currentIndex: number = 0
  
  // UI状态
  @State isPlaying: boolean = false
  @State currentPosition: number = 0
  @State duration: number = 0
  @State volume: number = 0.5
  @State isLooping: boolean = false
  
  // 定时器ID
  private positionTimerId: number = -1
  
  build() {
    Column({ space: 20 }) {
      // 标题
      Text('鸿蒙音乐播放器')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 50, bottom: 30 })
      
      // 当前播放音乐信息
      Column() {
        if (this.currentIndex >= 0 && this.currentIndex < this.musicList.length) {
          Text(this.musicList[this.currentIndex].title)
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
        } else {
          Text('未选择音乐')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
        }
        
        // 进度条
        Row({ space: 10 }) {
          Text(this.formatTime(this.currentPosition))
            .fontSize(14)
          
          Slider({
            value: this.currentPosition,
            min: 0,
            max: this.duration > 0 ? this.duration : 100,
            step: 1
          })
            .width('60%')
            .onChange((value: number) => {
              this.seekToPosition(value)
            })
          
          Text(this.formatTime(this.duration))
            .fontSize(14)
        }
        .width('90%')
        .margin({ top: 20, bottom: 20 })
      }
      .width('90%')
      .padding(20)
      .backgroundColor('#f0f0f0')
      .borderRadius(15)
      
      // 播放控制按钮
      Row({ space: 15 }) {
        Button({ type: ButtonType.Circle }) {
          Image($r('app.media.ic_previous')).width(24).height(24)
        }
        .width(60)
        .height(60)
        .onClick(() => this.playPrevious())
        
        Button({ type: ButtonType.Circle }) {
          Image($r(this.isPlaying ? 'app.media.ic_pause' : 'app.media.ic_play')).width(30).height(30)
        }
        .width(80)
        .height(80)
        .backgroundColor('#007DFF')
        .onClick(() => {
          if (this.isPlaying) {
            this.pauseMusic()
          } else {
            this.playMusic()
          }
        })
        
        Button({ type: ButtonType.Circle }) {
          Image($r('app.media.ic_next')).width(24).height(24)
        }
        .width(60)
        .height(60)
        .onClick(() => this.playNext())
      }
      .margin({ top: 20, bottom: 30 })
      
      // 功能控制
      Row({ space: 20 }) {
        Column() {
          Text('音量')
            .fontSize(14)
            .margin({ bottom: 10 })
          
          Slider({
            value: this.volume * 100,
            min: 0,
            max: 100,
            step: 1
          })
            .width(150)
            .onChange((value: number) => {
              this.setVolume(value / 100)
            })
        }
        
        Toggle({ type: ToggleType.Checkbox, isOn: this.isLooping })
          .onChange((isOn: boolean) => {
            this.setLoop(isOn)
          })
        
        Text('循环播放')
          .fontSize(14)
      }
      .width('90%')
      .justifyContent(FlexAlign.SpaceAround)
      
      // 音乐列表
      Column() {
        Text('播放列表')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .alignSelf(ItemAlign.Start)
          .margin({ bottom: 10 })
        
        List() {
          ForEach(this.musicList, (item, index) => {
            ListItem() {
              Row() {
                Text(item.title)
                  .fontSize(16)
                  .fontColor(this.currentIndex === index ? '#007DFF' : '#000000')
                
                if (this.currentIndex === index && this.isPlaying) {
                  Image($r('app.media.ic_playing'))
                    .width(20)
                    .height(20)
                }
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)
              .padding(10)
              .backgroundColor(this.currentIndex === index ? '#e6f2ff' : '#ffffff')
              .borderRadius(8)
            }
            .onClick(() => {
              this.switchMusic(index)
            })
          })
        }
        .width('100%')
        .height(200)
      }
      .width('90%')
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
  }
  
  aboutToAppear() {
    this.initializePlayer()
  }
  
  aboutToDisappear() {
    this.releasePlayer()
  }
  
  async initializePlayer() {
    try {
      // 创建AVPlayer实例
      this.player = await media.createAVPlayer()
      
      // 设置状态变化监听
      this.player.on('stateChange', (state: media.AVPlayerState) => {
        console.log(`播放器状态: ${state}`)
        this.handleStateChange(state)
      })
      
      // 设置错误监听
      this.player.on('error', (error) => {
        console.error(`播放错误: ${error.code}, ${error.message}`)
        promptAction.showToast({
          message: `播放失败: ${error.message}`,
          duration: 3000
        })
      })
      
      // 加载第一首歌
      if (this.musicList.length > 0) {
        this.loadMusic(this.musicList[this.currentIndex])
      }
      
    } catch (error) {
      promptAction.showToast({
        message: `初始化播放器失败: ${error.message}`,
        duration: 3000
      })
    }
  }
  
  handleStateChange(state: media.AVPlayerState) {
    switch (state) {
      case 'initialized':
        this.player.prepare()
        break
      case 'prepared':
        // 获取音乐时长
        this.player.getDuration((err, duration) => {
          if (!err) {
            this.duration = duration
          }
        })
        break
      case 'playing':
        this.isPlaying = true
        this.startPositionTimer()
        break
      case 'paused':
        this.isPlaying = false
        this.stopPositionTimer()
        break
      case 'completed':
        this.isPlaying = false
        this.currentPosition = this.duration
        this.stopPositionTimer()
        
        // 如果不是循环播放,播放下一首
        if (!this.isLooping) {
          this.playNext()
        }
        break
      case 'stopped':
        this.isPlaying = false
        this.currentPosition = 0
        this.stopPositionTimer()
        break
      case 'error':
        this.isPlaying = false
        this.stopPositionTimer()
        break
    }
  }
  
  // 加载音乐
  async loadMusic(music: { id: number, title: string, url: string }) {
    if (!this.player) return
    
    try {
      // 如果有正在播放的音乐,先停止
      if (this.isPlaying) {
        this.player.stop()
        this.isPlaying = false
      }
      
      // 重置播放器
      this.player.reset()
      
      // 设置新的音乐源
      this.player.url = music.url
      
      // 设置循环状态
      this.player.setLooping(this.isLooping)
      
      // 设置音量
      this.player.setVolume(this.volume, this.volume)
      
      // 重置UI状态
      this.currentPosition = 0
      this.duration = 0
      
    } catch (error) {
      promptAction.showToast({
        message: `加载音乐失败: ${error.message}`,
        duration: 3000
      })
    }
  }
  
  // 开始播放
  playMusic() {
    if (this.player) {
      this.player.play()
    }
  }
  
  // 暂停播放
  pauseMusic() {
    if (this.player) {
      this.player.pause()
    }
  }
  
  // 播放上一首
  playPrevious() {
    if (this.musicList.length === 0) return
    
    this.currentIndex = (this.currentIndex - 1 + this.musicList.length) % this.musicList.length
    this.loadMusic(this.musicList[this.currentIndex])
    this.playMusic()
  }
  
  // 播放下一首
  playNext() {
    if (this.musicList.length === 0) return
    
    this.currentIndex = (this.currentIndex + 1) % this.musicList.length
    this.loadMusic(this.musicList[this.currentIndex])
    this.playMusic()
  }
  
  // 切换到指定音乐
  switchMusic(index: number) {
    if (index >= 0 && index < this.musicList.length && index !== this.currentIndex) {
      this.currentIndex = index
      this.loadMusic(this.musicList[this.currentIndex])
      this.playMusic()
    }
  }
  
  // 跳转到指定位置
  seekToPosition(position: number) {
    if (this.player) {
      this.player.seek(position, 'closest')
      this.currentPosition = position
    }
  }
  
  // 设置音量
  setVolume(volume: number) {
    if (this.player) {
      this.volume = volume
      this.player.setVolume(volume, volume)
    }
  }
  
  // 设置循环播放
  setLoop(loop: boolean) {
    this.isLooping = loop
    if (this.player) {
      this.player.setLooping(loop)
    }
  }
  
  // 开始定时获取播放位置
  startPositionTimer() {
    if (this.positionTimerId !== -1) {
      clearInterval(this.positionTimerId)
    }
    
    this.positionTimerId = setInterval(() => {
      if (this.player) {
        this.player.getCurrentTime((err, time) => {
          if (!err) {
            this.currentPosition = time
          }
        })
      }
    }, 1000)
  }
  
  // 停止定时器
  stopPositionTimer() {
    if (this.positionTimerId !== -1) {
      clearInterval(this.positionTimerId)
      this.positionTimerId = -1
    }
  }
  
  // 释放播放器资源
  releasePlayer() {
    this.stopPositionTimer()
    if (this.player) {
      this.player.off('stateChange')
      this.player.off('error')
      this.player.stop()
      this.player.release()
      this.player = null
    }
  }
  
  // 格式化时间显示
  formatTime(ms: number): string {
    if (isNaN(ms) || ms < 0) return '00:00'
    
    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')}`
  }
}

十一、使用AVPlayer的最佳实践

  1. 资源管理:始终在组件销毁时释放AVPlayer实例,防止内存泄漏。

  2. 错误处理:全面监听和处理可能出现的播放错误,提供用户友好的错误提示。

  3. 状态管理:根据播放器状态更新UI,确保用户界面与播放器状态一致。

  4. 性能优化

    • 避免频繁创建和销毁AVPlayer实例
    • 使用合适的缓冲策略
    • 优化UI更新频率,减少不必要的重绘
  5. 兼容性:针对不同版本的API做好兼容处理,尤其是从API 11到API 15的变化。

十二、总结

本教程详细介绍了如何使用鸿蒙OS的AVPlayer API开发一个功能完善的音乐播放器应用。通过学习和实践,你应该能够掌握:

  1. AVPlayer的基本使用流程和生命周期管理
  2. 音乐播放器UI设计和实现
  3. 播放控制、进度显示、列表管理等功能实现
  4. 错误处理和异常情况的应对策略

希望本教程对你开发鸿蒙OS音乐播放器应用有所帮助!如有问题,欢迎在评论区留言讨论。

参考资料

  1. 鸿蒙OS官方文档 - AVPlayer
  2. 使用AVPlayer播放音频
  3. 鸿蒙OS多媒体开发指南
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值