在这里插入图片描述
在这里插入图片描述

一、引言

多媒体能力是移动应用的重要组成。无论是音乐播放器、视频应用、语音助手,还是拍照美颜,都离不开多媒体开发。HarmonyOS 提供了完整的媒体框架,涵盖音频播放、视频播放、音频录制、图像处理、相机等能力。

本文将以一个暗红金风格的音乐播放器页面为主线,深入讲解 HarmonyOS 音频播放的开发流程,并通过大量代码示例帮助读者掌握多媒体开发的核心技能。

二、媒体框架概览

HarmonyOS 媒体框架(@kit.MediaKit)提供以下核心能力:

能力 模块 说明
音频播放 @ohos.multimedia.audio 音频播放器、音频管理
视频播放 @ohos.multimedia.media 视频播放器
音频录制 @ohos.multimedia.media 麦克风录音
图像处理 @ohos.multimedia.image 图片解码、编辑
相机 @ohos.multimedia.camera 相机拍照、录像
媒体库 @ohos.file.photoAccessHelper 访问系统媒体库

2.1 音频播放的两种方式

HarmonyOS 提供两种音频播放方式:

  1. AVPlayer:高级播放器,支持本地和在线音频、视频播放,功能强大。
  2. AudioRenderer:底层音频渲染器,适合实时音频流处理。

本文主要讲解 AVPlayer 的使用。

三、AVPlayer 核心概念

3.1 播放状态机

AVPlayer 有一个完整的播放状态机:

状态 说明
idle 初始状态
initialized 已初始化,设置了数据源
prepared 已准备好,可以播放
playing 播放中
paused 已暂停
completed 播放完成
stopped 已停止
released 已释放资源
error 播放出错

3.2 播放流程

AVPlayer 的标准播放流程:

  1. 创建 AVPlayer 实例。
  2. 注册状态监听(stateChange)。
  3. 设置播放源(urlfdSrc)。
  4. 准备播放(prepare)。
  5. 播放 / 暂停 / 跳转。
  6. 播放完成或停止后释放资源(release)。

四、实战代码:音乐播放器页面

下面我们实现一个音乐播放器风格的演示页面。由于示例环境没有真实音频文件,页面以 UI 演示和状态管理为主,同时给出完整的 AVPlayer 播放代码。

4.1 定义数据结构

interface Track {
  id: number;
  title: string;
  artist: string;
  duration: string;
}

代码说明:

Track 接口描述一首歌曲的数据:

  • id:歌曲编号,用于标识和选中。
  • title:歌曲名称。
  • artist:歌手名称。
  • duration:歌曲时长(展示用字符串)。

4.2 组件状态定义

@Entry
@Component
struct MultimediaPage {
  @State tracks: Track[] = [
    { id: 1, title: 'HarmonyOS 序曲', artist: 'ArkTS 交响乐团', duration: '03:24' },
    { id: 2, title: '状态管理蓝调', artist: 'State 乐队', duration: '04:10' },
    { id: 3, title: '并发进行曲', artist: 'TaskPool 合唱团', duration: '02:58' },
    { id: 4, title: '动画圆舞曲', artist: 'Anim 三重奏', duration: '05:02' },
    { id: 5, title: '网络协奏曲', artist: 'HTTP 四重奏', duration: '03:47' },
    { id: 6, title: '持久化小夜曲', artist: 'Prefs 独奏', duration: '02:31' }
  ];
  @State currentId: number = 1;
  @State playing: boolean = false;
  @State progress: number = 0;

代码说明:

  • @State tracks:歌曲列表数据。
  • @State currentId:当前选中的歌曲编号。
  • @State playing:播放状态标志。
  • @State progress:播放进度(0-100)。

4.3 播放控制方法

togglePlay(id: number): void {
  this.currentId = id;
  this.playing = !this.playing;
  if (this.playing) {
    // 模拟进度推进
    this.progress = 20;
  } else {
    this.progress = 0;
  }
}

代码说明:

togglePlay 方法处理播放/暂停切换:

  • 设置当前选中歌曲 currentId
  • 切换播放状态 playing
  • 演示环境中用 progress 模拟播放进度。

4.4 完整 AVPlayer 播放代码

在真实项目中,音频播放的核心代码如下:

import { media } from '@kit.MediaKit';
import { audio } from '@kit.AudioKit';

// 创建音频会话(可选,用于音量控制等)
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 audioRendererInfo: audio.AudioRendererInfo = {
  usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
  rendererFlags: 0
};

// 创建 AVPlayer
let avPlayer: media.AVPlayer = await media.createAVPlayer();

// 注册状态监听
avPlayer.on('stateChange', (state: string) => {
  switch (state) {
    case 'idle':
      console.info('状态:空闲');
      break;
    case 'initialized':
      console.info('状态:已初始化');
      break;
    case 'prepared':
      console.info('状态:已准备');
      break;
    case 'playing':
      console.info('状态:播放中');
      break;
    case 'paused':
      console.info('状态:已暂停');
      break;
    case 'completed':
      console.info('状态:播放完成');
      break;
    case 'error':
      console.error('状态:播放出错');
      break;
  }
});

// 设置播放源(本地文件)
avPlayer.url = 'file:///data/storage/el2/base/files/music.mp3';

// 准备播放
await avPlayer.prepare();

// 开始播放
avPlayer.play();

// 暂停
avPlayer.pause();

// 跳转到指定位置(毫秒)
avPlayer.seek(30000);

// 停止
avPlayer.stop();

// 释放资源
await avPlayer.release();

代码说明:

  1. 创建播放器media.createAVPlayer() 创建 AVPlayer 实例。

  2. 状态监听avPlayer.on('stateChange', callback) 注册状态变化监听,根据状态执行相应逻辑。这是 AVPlayer 使用中最重要的回调,所有操作都应在正确的状态下进行。

  3. 设置播放源avPlayer.url 设置音频文件地址。支持:

    • 本地文件:file:// 协议。
    • 网络资源:http://https:// 协议。
    • 应用沙箱文件:使用 fdSrc 属性传入文件描述符。
  4. 准备播放avPlayer.prepare() 异步准备,完成后状态变为 prepared

  5. 播放控制play()pause()seek()stop() 控制播放。

  6. 资源释放release() 释放播放器资源,必须在不再使用时调用,避免资源泄漏。

4.5 播放进度监听

// 监听时间更新
avPlayer.on('timeUpdate', (time: number) => {
  // time 为当前播放位置(毫秒)
  const duration = avPlayer.duration;  // 总时长(毫秒)
  const percent = (time / duration) * 100;
  console.info(`播放进度: ${percent.toFixed(1)}%`);
});

// 监听播放结束
avPlayer.on('playbackCompleted', () => {
  console.info('播放完成');
});

代码说明:

  • timeUpdate 事件在播放位置变化时触发,参数是当前播放位置(毫秒)。
  • playbackCompleted 事件在播放完成时触发。
  • 通过这两个事件可以实现进度条更新和自动播放下一曲等功能。

4.6 构建 UI

build() {
  Scroll() {
    Column({ space: 16 }) {
      // 顶部暗红金标题
      Column() {
        Text('MUSIC')
          .fontSize(12)
          .fontColor('#FFD700')
          .letterSpacing(8)
        Text('多媒体播放')
          .fontSize(26)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .margin({ top: 6 })
        Text('AVPlayer · 音频播放器')
          .fontSize(12)
          .fontColor('#FFD700')
          .margin({ top: 6 })
      }
      .width('100%')
      .padding({ top: 48, bottom: 30 })
      .backgroundColor('#1A0A0A')

      // 唱片封面 + 圆形播放按钮
      Row({ space: 20 }) {
        // 唱片
        Stack({ alignContent: Alignment.Center }) {
          Circle({ width: 110, height: 110 })
            .fill('#2B0A0A')
            .stroke('#FFD700')
            .strokeWidth(2)
          Circle({ width: 60, height: 60 })
            .fill('#4A1A1A')
          Circle({ width: 18, height: 18 })
            .fill('#FFD700')
        }
        .width(110)
        .height(110)

        Column({ space: 8 }) {
          Text(this.tracks[this.currentId - 1].title)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
          Text(this.tracks[this.currentId - 1].artist)
            .fontSize(12)
            .fontColor('#FFD700')
          // 进度条
          Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
            .width(160)
            .color('#FFD700')
            .backgroundColor('#3A1A1A')
        }
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#2B0A0A')
      .borderRadius(20)
      .border({ width: 1, color: '#FFD70044' })

代码说明:

唱片封面区:

  1. 唱片图形:使用三层 Stack 叠放的 Circle 组件绘制唱片:

    • 外层:深红圆盘,金色描边。
    • 中层:略小的深色圆盘。
    • 内层:金色小圆点(唱片中心标签)。
    • 通过嵌套圆环模拟真实唱片的外观。
  2. 歌曲信息:显示当前选中歌曲的标题和歌手,标题白色、歌手金色。

  3. 进度条Progress 组件显示播放进度,value 绑定 progress 状态,金色进度条在深红背景上非常醒目。

  4. 风格统一:整个页面采用暗红(#1A0A0A、#2B0A0A)与金色(#FFD700)的搭配,营造复古唱片机的氛围。

      // 圆形播放按钮
      Button() {
        Text(this.playing ? '⏸' : '▶')
          .fontSize(28)
          .fontColor('#1A0A0A')
      }
      .width(64)
      .height(64)
      .backgroundColor('#FFD700')
      .borderRadius(32)
      .shadow({ radius: 16, color: '#88FFD700', offsetY: 0 })
      .onClick(() => { this.togglePlay(this.currentId); })

代码说明:

圆形播放/暂停按钮:

  • Button() 无文本参数,使用 Button() {} 自定义内容(图标文字)。
  • .borderRadius(32)(64 的一半)形成正圆。
  • 金色背景、深色图标,配合金色发光阴影。
  • 图标根据播放状态切换:播放中显示暂停符号(⏸),暂停时显示播放符号(▶)。
      // 播放列表表格
      Column() {
        Text('播放列表')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFD700')
          .alignSelf(ItemAlign.Start)
          .margin({ bottom: 8 })
        Row() {
          Text('#').width(36).fontSize(11).fontColor('#FFD700')
          Text('歌名').layoutWeight(1).fontSize(11).fontColor('#FFD700')
          Text('歌手').layoutWeight(1).fontSize(11).fontColor('#FFD700')
          Text('时长').width(50).fontSize(11).fontColor('#FFD700')
        }
        .width('100%')
        .padding({ top: 8, bottom: 8 })
        .border({ width: { bottom: 1 }, color: '#FFD70044' })

        ForEach(this.tracks, (track: Track) => {
          Row() {
            Text(`${track.id}`)
              .width(36)
              .fontSize(12)
              .fontColor(this.currentId === track.id ? '#FFD700' : '#888888')
              .fontWeight(this.currentId === track.id ? FontWeight.Bold : FontWeight.Normal)
            Text(track.title)
              .layoutWeight(1)
              .fontSize(12)
              .fontColor(this.currentId === track.id ? Color.White : '#BBBBBB')
            Text(track.artist)
              .layoutWeight(1)
              .fontSize(11)
              .fontColor('#888888')
            Text(track.duration)
              .width(50)
              .fontSize(11)
              .fontColor('#FFD700')
          }
          .width('100%')
          .padding({ top: 12, bottom: 12 })
          .border({ width: { bottom: 1 }, color: '#3A1A1A' })
          .onClick(() => { this.togglePlay(track.id); })
        })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#1A0A0A')
      .borderRadius(14)
      .border({ width: 1, color: '#FFD70033' })

代码说明:

播放列表表格:

  1. 表头行:包含序号、歌名、歌手、时长四列,全部使用金色文字。

  2. 数据行ForEach 遍历歌曲列表:

    • 当前选中歌曲(currentId === track.id)的序号和歌名使用金色/白色高亮,其他歌曲使用灰色。
    • 每行可点击,点击后调用 togglePlay 切换播放。
  3. 高亮逻辑:通过条件判断 this.currentId === track.id 控制文字颜色和字重,实现"当前播放"的视觉标识。

  4. 表格样式:行间用深红色边框分隔,与页面整体暗色风格统一。

五、音频录制

除了播放,HarmonyOS 还支持音频录制。使用 AudioCapturer 录制麦克风音频:

import { audio } from '@kit.AudioKit';

async function startRecord(): Promise<void> {
  const audioManager = audio.getAudioManager();
  // 申请录音权限
  // 需要在 module.json5 中配置 ohos.permission.MICROPHONE

  const audioCapturer = await audio.createAudioCapturer({
    streamInfo: {
      samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
      channels: audio.AudioChannel.CHANNEL_1,
      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
    },
    capturerInfo: {
      source: audio.SourceType.SOURCE_TYPE_MIC,
      capturerFlags: 0
    }
  });

  await audioCapturer.start();
  // 读取音频数据
  const buffer = new ArrayBuffer(44100 * 2);
  const readSize = await audioCapturer.read(buffer, true);
  // 处理音频数据...
}

六、媒体权限配置

使用多媒体能力需要配置相应权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "用于录制音频",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "用于读取媒体文件",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

代码说明:

  • ohos.permission.MICROPHONE:麦克风权限,录制音频必需。
  • ohos.permission.READ_MEDIA:读取媒体库权限。
  • 动态权限还需要在运行时通过 requestPermissionsFromUser 申请。

七、最佳实践

7.1 资源管理

AVPlayer 使用完成后必须调用 release() 释放资源,否则会导致内存泄漏。

7.2 状态机管理

AVPlayer 的所有操作都必须在正确的状态下执行。例如,play() 必须在 prepared 状态之后调用。建议封装状态管理逻辑。

7.3 播放器封装

建议将 AVPlayer 封装为独立的播放器类,统一管理状态和事件:

export class MusicPlayer {
  private avPlayer: media.AVPlayer | null = null;

  async init(url: string): Promise<void> {
    this.avPlayer = await media.createAVPlayer();
    this.avPlayer.url = url;
    await this.avPlayer.prepare();
  }

  play(): void {
    this.avPlayer?.play();
  }

  pause(): void {
    this.avPlayer?.pause();
  }

  async release(): Promise<void> {
    await this.avPlayer?.release();
    this.avPlayer = null;
  }
}

7.4 后台播放

如果应用需要在后台播放音频,需要申请后台任务权限并配置:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "用于后台播放音频",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

八、常见问题

8.1 播放无声音

原因:可能是音频焦点未获取、音量设置过低、或播放源错误。

解决:检查播放源是否有效,设置合适的音量,必要时获取音频焦点。

8.2 播放器状态错误

原因:在错误的状态下调用了操作(如未 prepare 就 play)。

解决:严格遵循状态机流程,监听 stateChange 事件。

8.3 网络音频卡顿

原因:网络不稳定或缓冲策略不合理。

解决:使用合适的缓冲策略,或预先加载音频。

九、总结

本文深入讲解了 HarmonyOS 多媒体开发,重点介绍了音频播放技术。我们实现了一个暗红金风格的音乐播放器页面,并给出了完整的 AVPlayer 播放代码。

核心要点回顾:

  1. AVPlayer 是高级媒体播放器,支持本地和网络音频。
  2. 播放流程:创建 → 监听状态 → 设置源 → prepare → play。
  3. 状态机管理是 AVPlayer 使用的关键。
  4. 使用完成后必须 release() 释放资源。
  5. 录制音频使用 AudioCapturer,需要麦克风权限。
  6. 多媒体能力需要配置相应权限。

多媒体开发让应用更加丰富多彩,掌握它能构建音乐、视频、语音等丰富的应用体验。下一篇我们将讲解 HarmonyOS 并发编程。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐