在这里插入图片描述

每日一句正能量

“比别人的目光更可怕的是你那颗在意他人目光的心。”
你以为是别人的眼光困住了你,其实是你心里那个“在意”在作祟。外界的目光本身没有力量,是你赋予了它们审判权。当你不再把别人的看法当成镜子,那些目光就会像风吹过树叶,有声音,但无重量。

摘要

在移动应用开发中,后台音乐播放是音频类应用的核心能力。本文系统讲解 HarmonyOS 提供的后台音频播放方案,从 AVSession 播控中心接入、长时任务申请、音频焦点管理三个维度出发,结合 ArkTS 代码实战,帮助开发者构建稳定可靠的后台音乐播放系统。


一、引言:后台音乐播放的工程挑战

音乐播放器、有声书、播客等音频类应用,后台播放是刚需。然而,后台音频播放涉及多个系统能力的协同,开发者往往面临以下挑战:

  • 退后台即静音:应用切到后台或锁屏后,音频被系统强制暂停,用户无法继续收听。
  • 播控中心失联:锁屏界面和通知栏无法显示播放控制按钮,用户体验割裂。
  • 多应用音频冲突:前台打开短视频或接听电话时,后台音乐没有正确暂停或恢复。
  • 长时任务管理混乱:播放时申请了长时任务,暂停后忘记释放,导致系统资源浪费。

HarmonyOS 提供了 AVSession(音视频播控服务)BackgroundTaskManager(后台任务管理)AudioSession(音频焦点管理) 三大核心能力,三者协同才能构建完整的后台音乐播放体验。本文将系统梳理这三个能力的接入方法、交互逻辑和生产级最佳实践。


二、能力架构:三位一体的后台播放方案

HarmonyOS 后台音乐播放需要三个核心能力协同工作:

在这里插入图片描述

能力 职责 不接入的后果
AVSession 向系统播控中心注册媒体会话,同步元数据和播放状态 锁屏/通知栏不显示播控卡片,无法接收系统播控指令
长时任务 申请 AUDIO_PLAYBACK 类型后台任务,防止进程被回收 退后台后音频被系统静音并冻结,进程可能被回收
音频焦点 管理多应用音频共存策略,处理打断和恢复事件 前台应用播放音频时,后台音乐继续播放造成混音

核心原则:当应用需要后台播放 STREAM_USAGE_MUSICSTREAM_USAGE_MOVIESTREAM_USAGE_AUDIOBOOKSTREAM_USAGE_GAME 类型的音频时,必须同时接入 AVSession 和申请长时任务。不满足此规范的应用,退至后台时会被系统静音并冻结,直到应用重新切回前台才会恢复。


三、AVSession 播控中心接入

3.1 AVSession 核心概念

AVSession(Audio/Video Session)是 HarmonyOS 提供的音视频播控服务,在应用与系统控制器(播控中心、语音助手)之间建立标准化的"会话"。应用只需向系统注册播放状态和控制命令,无需单独适配各种外设,即可实现"一次开发、处处可控"。

AVSession 的两种角色

  • 提供方(Provider):实际播放音视频的应用,如音乐播放器、视频 App。
  • 控制方(Controller):向提供方发送播控指令的组件,如系统播控中心、语音助手、穿戴设备。

本文聚焦提供方开发,即音乐播放器如何接入 AVSession 并向系统注册自身。

3.2 接入流程

在这里插入图片描述

接入 AVSession 的完整流程如下:

  1. 创建 AVSession:在应用启动或开始播放业务前创建会话。
  2. 设置媒体元数据:同步歌曲标题、歌手、专辑、封面图等信息。
  3. 注册播控命令监听:响应系统下发的播放、暂停、切歌、 seek 等指令。
  4. 激活会话:调用 activate() 使会话生效。
  5. 同步播放状态:播放状态变化时及时上报给系统。
  6. 接收并执行播控指令:在监听器中执行实际播放逻辑,并同步状态。

3.3 代码实战:AVSession 接入

// src/manager/AVSessionManager.ets

import { avSession } from '@kit.AVSessionKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

/**
 * 歌曲信息模型
 */
export interface SongInfo {
  assetId: string;        // 唯一标识
  title: string;          // 歌曲标题
  artist: string;         // 歌手
  album: string;          // 专辑
  duration: number;       // 总时长(ms)
  coverUrl?: string;      // 封面图URL
  mediaUri: string;       // 音频资源地址
}

/**
 * AVSession 管理器
 * 负责与系统播控中心交互,注册媒体会话并同步状态
 */
export class AVSessionManager {
  private static instance: AVSessionManager | null = null;
  private avSession: avSession.AVSession | undefined = undefined;
  private currentSong: SongInfo | null = null;

  // 播放状态回调(供外部订阅)
  private onPlayCallback: (() => void) | null = null;
  private onPauseCallback: (() => void) | null = null;
  private onPlayNextCallback: (() => void) | null = null;
  private onPlayPreviousCallback: (() => void) | null = null;
  private onSeekCallback: ((time: number) => void) | null = null;

  public static getInstance(): AVSessionManager {
    if (!AVSessionManager.instance) {
      AVSessionManager.instance = new AVSessionManager();
    }
    return AVSessionManager.instance;
  }

  /**
   * 创建并激活 AVSession
   */
  public async createSession(context: common.UIAbilityContext): Promise<void> {
    if (this.avSession) {
      console.info('[AVSession] 会话已存在,跳过创建');
      return;
    }

    try {
      this.avSession = await avSession.createAVSession(context, 'MusicPlayer', 'audio');
      console.info('[AVSession] 会话创建成功');

      // 注册播控命令监听
      this.registerCommandListeners();

      // 激活会话
      await this.avSession.activate();
      console.info('[AVSession] 会话已激活');

      // 设置后台播放模式
      await this.avSession.setBackgroundPlayMode(avSession.BackgroundPlayMode.ENABLE_BACKGROUND_PLAY);
      console.info('[AVSession] 后台播放模式已设置');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[AVSession] 创建会话失败: ${err.code}, ${err.message}`);
      throw error;
    }
  }

  /**
   * 注册播控命令监听器
   */
  private registerCommandListeners(): void {
    if (!this.avSession) return;

    // 播放指令
    this.avSession.on('play', () => {
      console.info('[AVSession] 收到播控中心: play');
      this.onPlayCallback?.();
    });

    // 暂停指令
    this.avSession.on('pause', () => {
      console.info('[AVSession] 收到播控中心: pause');
      this.onPauseCallback?.();
    });

    // 下一首
    this.avSession.on('playNext', () => {
      console.info('[AVSession] 收到播控中心: playNext');
      this.onPlayNextCallback?.();
    });

    // 上一首
    this.avSession.on('playPrevious', () => {
      console.info('[AVSession] 收到播控中心: playPrevious');
      this.onPlayPreviousCallback?.();
    });

    // 进度跳转
    this.avSession.on('seek', (time: number) => {
      console.info(`[AVSession] 收到播控中心: seek to ${time}ms`);
      this.onSeekCallback?.(time);
    });

    // 快进
    this.avSession.on('fastForward', () => {
      console.info('[AVSession] 收到播控中心: fastForward');
      this.onSeekCallback?.((this.currentSong?.duration || 0) * 0.1);
    });

    // 快退
    this.avSession.on('rewind', () => {
      console.info('[AVSession] 收到播控中心: rewind');
      this.onSeekCallback?.(-(this.currentSong?.duration || 0) * 0.1);
    });

    // 设置播放模式(循环/随机)
    this.avSession.on('setLoopMode', (mode: avSession.LoopMode) => {
      console.info(`[AVSession] 收到播控中心: setLoopMode=${mode}`);
    });

    // 收藏
    this.avSession.on('toggleFavorite', (assetId: string) => {
      console.info(`[AVSession] 收到播控中心: toggleFavorite assetId=${assetId}`);
    });
  }

  /**
   * 设置当前播放歌曲的元数据
   */
  public async setMediaMetadata(song: SongInfo): Promise<void> {
    if (!this.avSession) return;
    this.currentSong = song;

    const metadata: avSession.AVMetadata = {
      assetId: song.assetId,
      title: song.title,
      artist: song.artist,
      album: song.album,
      duration: song.duration,
      mediaType: avSession.AVMediaType.AUDIO,
      displayTags: avSession.DisplayTag.TAG_AUDIO_VIVID
    };

    try {
      await this.avSession.setAVMetadata(metadata);
      console.info(`[AVSession] 元数据已同步: ${song.title} - ${song.artist}`);
    } catch (error) {
      console.error('[AVSession] 设置元数据失败:', error);
    }
  }

  /**
   * 同步播放状态到播控中心
   */
  public async setPlaybackState(state: avSession.AVPlaybackState): Promise<void> {
    if (!this.avSession) return;
    try {
      await this.avSession.setAVPlaybackState(state);
      console.info(`[AVSession] 播放状态已同步: state=${state.state}`);
    } catch (error) {
      console.error('[AVSession] 同步播放状态失败:', error);
    }
  }

  /**
   * 设置播放状态为播放中
   */
  public async setPlayingState(position: number, speed: number = 1.0): Promise<void> {
    const state: avSession.AVPlaybackState = {
      state: avSession.PlaybackState.PLAYBACK_STATE_PLAY,
      position: { elapsedTime: position, updateTime: Date.now() },
      speed: speed
    };
    await this.setPlaybackState(state);
  }

  /**
   * 设置播放状态为暂停
   */
  public async setPausedState(position: number): Promise<void> {
    const state: avSession.AVPlaybackState = {
      state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
      position: { elapsedTime: position, updateTime: Date.now() }
    };
    await this.setPlaybackState(state);
  }

  /**
   * 设置播放状态为停止
   */
  public async setStoppedState(): Promise<void> {
    const state: avSession.AVPlaybackState = {
      state: avSession.PlaybackState.PLAYBACK_STATE_STOP
    };
    await this.setPlaybackState(state);
  }

  /**
   * 注册播放控制回调
   */
  public registerCallbacks(callbacks: {
    onPlay?: () => void;
    onPause?: () => void;
    onPlayNext?: () => void;
    onPlayPrevious?: () => void;
    onSeek?: (time: number) => void;
  }): void {
    this.onPlayCallback = callbacks.onPlay || null;
    this.onPauseCallback = callbacks.onPause || null;
    this.onPlayNextCallback = callbacks.onPlayNext || null;
    this.onPlayPreviousCallback = callbacks.onPlayPrevious || null;
    this.onSeekCallback = callbacks.onSeek || null;
  }

  /**
   * 释放 AVSession
   */
  public async release(): Promise<void> {
    if (!this.avSession) return;
    try {
      await this.avSession.deactivate();
      await this.avSession.destroy();
      this.avSession = undefined;
      console.info('[AVSession] 会话已释放');
    } catch (error) {
      console.error('[AVSession] 释放会话失败:', error);
    }
  }
}

3.4 关键注意事项

  1. 创建时机:建议在应用启动或开始播放业务前创建 AVSession,避免频繁创建和释放影响播放连续性。
  2. 对象保持:后台播放期间需确保 AVSession 对象实例一直存在,使用类成员变量而非局部变量保存。
  3. 状态同步:应用必须及时响应播控指令并同步播放状态,否则播控中心会显示"假播放"(进度走但实际已暂停)。
  4. 元数据完整性:必须设置标题、副标题/歌手、封面图等元数据,否则播控中心展示不完整。

四、长时任务申请与管理

4.1 为什么需要长时任务

即使正确接入了 AVSession,如果未申请长时任务,应用退至后台后仍会被系统静音并冻结。长时任务是应用在后台继续执行播放逻辑的"权限通行证"。

4.2 长时任务生命周期管理

长时任务的管理需要遵循以下规范:

  • 播放时申请:开始播放时申请 AUDIO_PLAYBACK 类型长时任务。
  • 暂停时释放:用户主动点击暂停时,及时取消长时任务。
  • 恢复时重新申请:用户再次点击播放时,重新申请长时任务。
  • 被打断时处理:音频在后台播放时被打断(如焦点打断),系统会自行检测并冻结或取消长时任务。当应用重启音频播放时,需要再次申请。
  • 投播时管理:通过 AVSession 投播组件后台投播时,开始投播申请长时任务,断开投播时取消。

4.3 代码实战:长时任务管理

// src/manager/BackgroundTaskManager.ets

import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

/**
 * 后台长时任务管理器
 */
export class AudioBackgroundTaskManager {
  private static instance: AudioBackgroundTaskManager | null = null;
  private isRunning: boolean = false;

  public static getInstance(): AudioBackgroundTaskManager {
    if (!AudioBackgroundTaskManager.instance) {
      AudioBackgroundTaskManager.instance = new AudioBackgroundTaskManager();
    }
    return AudioBackgroundTaskManager.instance;
  }

  /**
   * 启动 AUDIO_PLAYBACK 长时任务
   */
  public async startAudioPlayback(context: common.UIAbilityContext): Promise<void> {
    if (this.isRunning) {
      console.info('[BackgroundTask] 长时任务已在运行');
      return;
    }

    try {
      const wantAgentObj = await wantAgent.getWantAgent(context, {
        wants: [{
          bundleName: context.abilityInfo.bundleName,
          abilityName: context.abilityInfo.name
        }],
        operationType: wantAgent.OperationType.START_ABILITIES,
        requestCode: 0,
        wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
      });

      await backgroundTaskManager.startBackgroundRunning(
        context,
        backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
        wantAgentObj
      );

      this.isRunning = true;
      console.info('[BackgroundTask] AUDIO_PLAYBACK 长时任务已启动');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[BackgroundTask] 启动长时任务失败: ${err.code}, ${err.message}`);
    }
  }

  /**
   * 停止 AUDIO_PLAYBACK 长时任务
   */
  public async stopAudioPlayback(context: common.UIAbilityContext): Promise<void> {
    if (!this.isRunning) return;

    try {
      await backgroundTaskManager.stopBackgroundRunning(context);
      this.isRunning = false;
      console.info('[BackgroundTask] AUDIO_PLAYBACK 长时任务已停止');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[BackgroundTask] 停止长时任务失败: ${err.code}, ${err.message}`);
    }
  }

  public getIsRunning(): boolean {
    return this.isRunning;
  }
}

五、音频焦点管理

5.1 为什么需要音频焦点

在多个音频流同时播放的场景下,如果系统不加管控,会造成多个音频混音播放,用户体验极差。HarmonyOS 预设了音频打断(InterruptEvent)策略,对所有播放音频流进行统一管理。

当其他音频流申请焦点时,系统会根据焦点策略进行仲裁,判定本音频流的焦点是否有变化,并通过音频焦点事件通知应用执行相应操作(如暂停、继续、降低音量、恢复音量等)。

在这里插入图片描述

5.2 焦点模式选择

HarmonyOS 提供了四种并发模式,应用应根据业务场景选择合适的策略:

并发模式 行为 适用场景
CONCURRENCY_DEFAULT 使用系统默认策略 不关心并发的场景
CONCURRENCY_MIX_WITH_OTHERS 与其他音频同时播放 导航播报、背景音效
CONCURRENCY_DUCK_OTHERS 自己播放时压低其他音量 语音助手、语音消息
CONCURRENCY_PAUSE_OTHERS 暂停其他音频流 音乐播放器、有声书

优先级规则STOP > PAUSE > DUCK > PLAYBOTH。如果指定的策略优先级高于系统默认值,策略不会生效。

5.3 代码实战:音频焦点管理

// src/manager/AudioFocusManager.ets

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

/**
 * 音频焦点管理器
 * 负责管理应用音频焦点,处理打断和恢复事件
 */
export class AudioFocusManager {
  private static instance: AudioFocusManager | null = null;
  private sessionManager: audio.AudioSessionManager | null = null;
  private isActive: boolean = false;
  private onFocusChanged?: (event: audio.AudioSessionStateChangedEvent) => void;

  public static getInstance(): AudioFocusManager {
    if (!AudioFocusManager.instance) {
      AudioFocusManager.instance = new AudioFocusManager();
    }
    return AudioFocusManager.instance;
  }

  private constructor() {
    const audioManager = audio.getAudioManager();
    this.sessionManager = audioManager.getSessionManager();
  }

  /**
   * 激活音频会话(申请焦点)
   * 适用于音乐播放器、有声书等需要连续播放的场景
   */
  public async activateWithScene(
    mode: audio.AudioConcurrencyMode = audio.AudioConcurrencyMode.CONCURRENCY_PAUSE_OTHERS
  ): Promise<void> {
    if (!this.sessionManager) {
      throw new Error('AudioSessionManager 未初始化');
    }
    if (this.isActive) {
      console.info('[AudioFocus] 会话已激活');
      return;
    }

    try {
      const scene: audio.AudioSessionScene = { concurrencyMode: mode };
      this.sessionManager.setAudioSessionScene(scene);
      this.sessionManager.on('audioSessionStateChanged', this.handleStateChanged);
      await this.sessionManager.activateAudioSession();
      this.isActive = true;
      console.info(`[AudioFocus] 音频会话已激活,模式: ${mode}`);
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[AudioFocus] 激活失败: ${err.code}, ${err.message}`);
      throw error;
    }
  }

  /**
   * 停用音频会话
   */
  public async deactivate(): Promise<void> {
    if (!this.sessionManager || !this.isActive) return;
    try {
      this.sessionManager.off('audioSessionStateChanged', this.handleStateChanged);
      await this.sessionManager.deactivateAudioSession();
      this.isActive = false;
      console.info('[AudioFocus] 音频会话已停用');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[AudioFocus] 停用失败: ${err.code}, ${err.message}`);
    }
  }

  private handleStateChanged = (event: audio.AudioSessionStateChangedEvent): void => {
    console.info(`[AudioFocus] 状态变化: ${event.stateChangeHint}`);
    this.onFocusChanged?.(event);
  };

  public onFocusChange(callback: (event: audio.AudioSessionStateChangedEvent) => void): void {
    this.onFocusChanged = callback;
  }

  public getIsActive(): boolean {
    return this.isActive;
  }
}

5.4 打断恢复的关键流程

当应用收到 RESUME 事件时,系统只通知 session 不通知 AudioRenderer,需要手动执行三步恢复:

/**
 * 恢复播放三步走
 */
private async handleResume(): Promise<void> {
  if (!this.sessionManager) return;

  // 第一步:重设场景
  const scene: audio.AudioSessionScene = {
    concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_PAUSE_OTHERS
  };
  this.sessionManager.setAudioSessionScene(scene);

  // 第二步:重新激活
  await this.sessionManager.activateAudioSession();

  // 第三步:启动渲染器(或 AVPlayer)
  // this.avPlayer.play();

  console.info('[AudioFocus] 播放已恢复');
}

六、完整播放器集成

6.1 播放器状态机

// src/model/PlayerTypes.ets

export enum PlayerState {
  IDLE = 'idle',
  INITIALIZED = 'initialized',
  PREPARED = 'prepared',
  PLAYING = 'playing',
  PAUSED = 'paused',
  STOPPED = 'stopped',
  COMPLETED = 'completed',
  ERROR = 'error'
}

export interface PlayQueue {
  songs: SongInfo[];
  currentIndex: number;
  loopMode: 'loop_all' | 'loop_one' | 'shuffle';
}

6.2 音乐播放器核心实现

// src/manager/MusicPlayer.ets

import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import { AVSessionManager } from './AVSessionManager';
import { AudioBackgroundTaskManager } from './BackgroundTaskManager';
import { AudioFocusManager } from './AudioFocusManager';
import { SongInfo, PlayerState, PlayQueue } from '../model/PlayerTypes';

/**
 * 音乐播放器核心控制器
 * 集成 AVPlayer + AVSession + 长时任务 + 音频焦点
 */
export class MusicPlayer {
  private static instance: MusicPlayer | null = null;

  private avPlayer: media.AVPlayer | null = null;
  private avSessionMgr: AVSessionManager = AVSessionManager.getInstance();
  private bgTaskMgr: AudioBackgroundTaskManager = AudioBackgroundTaskManager.getInstance();
  private focusMgr: AudioFocusManager = AudioFocusManager.getInstance();

  private playQueue: PlayQueue = { songs: [], currentIndex: 0, loopMode: 'loop_all' };
  private playerState: PlayerState = PlayerState.IDLE;
  private currentPosition: number = 0;
  private playbackSpeed: number = 1.0;

  private stateListeners: Array<(state: PlayerState) => void> = [];
  private progressListeners: Array<(position: number, duration: number) => void> = [];

  public static getInstance(): MusicPlayer {
    if (!MusicPlayer.instance) {
      MusicPlayer.instance = new MusicPlayer();
    }
    return MusicPlayer.instance;
  }

  private constructor() {
    this.initAVSessionCallbacks();
  }

  /**
   * 初始化播放器
   */
  public async initialize(context: common.UIAbilityContext): Promise<void> {
    await this.avSessionMgr.createSession(context);
    this.avPlayer = await media.createAVPlayer();
    this.setupAVPlayerCallbacks();
    console.info('[MusicPlayer] 播放器初始化完成');
  }

  /**
   * 设置播放队列
   */
  public setPlaylist(songs: SongInfo[], startIndex: number = 0): void {
    this.playQueue = { songs, currentIndex: startIndex, loopMode: 'loop_all' };
  }

  /**
   * 播放指定歌曲
   */
  public async play(song?: SongInfo): Promise<void> {
    const targetSong = song || this.playQueue.songs[this.playQueue.currentIndex];
    if (!targetSong) {
      console.error('[MusicPlayer] 播放列表为空');
      return;
    }

    try {
      // 1. 申请音频焦点
      await this.focusMgr.activateWithScene();

      // 2. 申请后台长时任务
      const context = getContext(this) as common.UIAbilityContext;
      await this.bgTaskMgr.startAudioPlayback(context);

      // 3. 设置 AVSession 元数据
      await this.avSessionMgr.setMediaMetadata(targetSong);

      // 4. 准备并播放
      if (this.avPlayer) {
        if (this.playerState === PlayerState.PLAYING || this.playerState === PlayerState.PAUSED) {
          this.avPlayer.stop();
        }

        this.avPlayer.url = targetSong.mediaUri;
        await this.avPlayer.prepare();
        await this.avPlayer.play();

        this.playerState = PlayerState.PLAYING;
        this.currentPosition = 0;

        // 5. 同步播放状态到播控中心
        await this.avSessionMgr.setPlayingState(0, this.playbackSpeed);

        console.info(`[MusicPlayer] 开始播放: ${targetSong.title}`);
        this.notifyStateChange(PlayerState.PLAYING);
      }
    } catch (error) {
      this.playerState = PlayerState.ERROR;
      console.error('[MusicPlayer] 播放失败:', error);
      this.notifyStateChange(PlayerState.ERROR);
    }
  }

  /**
   * 暂停播放
   */
  public async pause(): Promise<void> {
    if (!this.avPlayer || this.playerState !== PlayerState.PLAYING) return;

    try {
      await this.avPlayer.pause();
      this.playerState = PlayerState.PAUSED;
      this.currentPosition = this.avPlayer.currentTime;

      await this.avSessionMgr.setPausedState(this.currentPosition);

      const context = getContext(this) as common.UIAbilityContext;
      await this.bgTaskMgr.stopAudioPlayback(context);

      console.info('[MusicPlayer] 播放已暂停');
      this.notifyStateChange(PlayerState.PAUSED);
    } catch (error) {
      console.error('[MusicPlayer] 暂停失败:', error);
    }
  }

  /**
   * 恢复播放
   */
  public async resume(): Promise<void> {
    if (!this.avPlayer || this.playerState !== PlayerState.PAUSED) return;

    try {
      await this.focusMgr.activateWithScene();
      const context = getContext(this) as common.UIAbilityContext;
      await this.bgTaskMgr.startAudioPlayback(context);

      await this.avPlayer.play();
      this.playerState = PlayerState.PLAYING;

      await this.avSessionMgr.setPlayingState(this.currentPosition, this.playbackSpeed);

      console.info('[MusicPlayer] 播放已恢复');
      this.notifyStateChange(PlayerState.PLAYING);
    } catch (error) {
      console.error('[MusicPlayer] 恢复播放失败:', error);
    }
  }

  /**
   * 停止播放
   */
  public async stop(): Promise<void> {
    if (!this.avPlayer) return;

    try {
      await this.avPlayer.stop();
      this.playerState = PlayerState.STOPPED;
      this.currentPosition = 0;

      await this.avSessionMgr.setStoppedState();

      const context = getContext(this) as common.UIAbilityContext;
      await this.bgTaskMgr.stopAudioPlayback(context);
      await this.focusMgr.deactivate();

      console.info('[MusicPlayer] 播放已停止');
      this.notifyStateChange(PlayerState.STOPPED);
    } catch (error) {
      console.error('[MusicPlayer] 停止失败:', error);
    }
  }

  /**
   * 播放下一首
   */
  public async playNext(): Promise<void> {
    if (this.playQueue.songs.length === 0) return;
    this.playQueue.currentIndex = (this.playQueue.currentIndex + 1) % this.playQueue.songs.length;
    await this.play();
  }

  /**
   * 播放上一首
   */
  public async playPrevious(): Promise<void> {
    if (this.playQueue.songs.length === 0) return;
    this.playQueue.currentIndex = (this.playQueue.currentIndex - 1 + this.playQueue.songs.length) % this.playQueue.songs.length;
    await this.play();
  }

  /**
   * 跳转到指定位置
   */
  public async seekTo(position: number): Promise<void> {
    if (!this.avPlayer) return;
    try {
      await this.avPlayer.seek(position);
      this.currentPosition = position;
      console.info(`[MusicPlayer] 跳转到: ${position}ms`);
    } catch (error) {
      console.error('[MusicPlayer] 跳转失败:', error);
    }
  }

  /**
   * 设置 AVPlayer 回调
   */
  private setupAVPlayerCallbacks(): void {
    if (!this.avPlayer) return;

    this.avPlayer.on('stateChange', async (state: media.AVPlayerState, reason: media.StateChangeReason) => {
      console.info(`[MusicPlayer] AVPlayer 状态变化: ${state}, reason: ${reason}`);

      if (state === 'completed') {
        this.playerState = PlayerState.COMPLETED;
        if (this.playQueue.loopMode === 'loop_one') {
          await this.play();
        } else {
          await this.playNext();
        }
      }
    });

    this.avPlayer.on('error', (err: BusinessError) => {
      console.error(`[MusicPlayer] AVPlayer 错误: ${err.code}, ${err.message}`);
      this.playerState = PlayerState.ERROR;
      this.notifyStateChange(PlayerState.ERROR);
    });

    this.avPlayer.on('timeUpdate', (time: number) => {
      this.currentPosition = time;
      const duration = this.avPlayer?.duration || 0;
      this.notifyProgressChange(time, duration);
    });
  }

  private initAVSessionCallbacks(): void {
    this.avSessionMgr.registerCallbacks({
      onPlay: () => this.resume(),
      onPause: () => this.pause(),
      onPlayNext: () => this.playNext(),
      onPlayPrevious: () => this.playPrevious(),
      onSeek: (time: number) => this.seekTo(time)
    });
  }

  public onStateChange(listener: (state: PlayerState) => void): void {
    this.stateListeners.push(listener);
  }

  public onProgressUpdate(listener: (position: number, duration: number) => void): void {
    this.progressListeners.push(listener);
  }

  private notifyStateChange(state: PlayerState): void {
    this.stateListeners.forEach(l => l(state));
  }

  private notifyProgressChange(position: number, duration: number): void {
    this.progressListeners.forEach(l => l(position, duration));
  }

  /**
   * 释放播放器资源
   */
  public async release(): Promise<void> {
    await this.stop();
    if (this.avPlayer) {
      this.avPlayer.release();
      this.avPlayer = null;
    }
    await this.avSessionMgr.release();
    console.info('[MusicPlayer] 播放器资源已释放');
  }
}

七、权限配置与模块声明

7.1 module.json5 配置

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      },
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
      },
      {
        "name": "ohos.permission.MEDIA_CONTROL"
      }
    ],
    "abilities": [
      {
        "name": "EntryAbility",
        "backgroundModes": [
          "audioPlayback"
        ]
      }
    ]
  }
}

7.2 关键配置说明

  • ohos.permission.KEEP_BACKGROUND_RUNNING:申请后台长时任务权限,必须声明。
  • ohos.permission.MEDIA_CONTROL:媒体控制权限,用于 AVSession 与系统播控中心交互。
  • backgroundModes: ["audioPlayback"]:声明应用支持音频播放后台模式。

八、测试验收清单

8.1 生命周期测试

  • 前台播放时切到桌面,音频是否继续播放
  • 息屏锁屏后,音频是否正常播放
  • 锁屏界面是否显示播控卡片(封面/标题/控制按钮)
  • 通知栏是否显示播放通知
  • 应用被系统回收后重新启动,播放状态是否正确恢复

8.2 播控中心交互测试

  • 点击锁屏播控卡片的播放/暂停,应用是否正确响应
  • 点击下一首/上一首,是否正确切换歌曲
  • 拖动进度条,是否正确跳转到对应位置
  • 播控中心显示的歌曲信息是否与当前播放一致
  • 播放状态变化时,播控中心是否及时更新

8.3 音频焦点测试

  • 后台播放时接听电话,音乐是否正确暂停
  • 电话挂断后,音乐是否自动恢复播放
  • 后台播放时打开短视频 App,音乐是否正确暂停
  • 关闭短视频 App 后,音乐是否自动恢复
  • 导航播报时,音乐是否正确压低音量(DUCK)
  • 导航播报结束后,音乐音量是否恢复正常

8.4 长时任务管理测试

  • 播放时是否正确申请了 AUDIO_PLAYBACK 长时任务
  • 暂停时是否正确释放了长时任务
  • 恢复播放时是否重新申请了长时任务
  • 长时间暂停后,进程是否被系统正常回收

九、常见问题与最佳实践

Q1:为什么退后台后音频被静音了?

A:检查三点:(1) 是否正确接入了 AVSession;(2) 是否申请了 AUDIO_PLAYBACK 长时任务;(3) module.json5 中是否声明了 audioPlayback 后台模式。三者缺一不可。

Q2:播控中心显示"假播放"(进度在走但实际已暂停)怎么办?

A:这是因为应用被系统挂起后,来不及调用 AVSession 同步暂停状态。确保在收到暂停指令或检测到播放中断时,立即调用 setAVPlaybackState 上报状态。同时检查长时任务是否有效。

Q3:如何支持蓝牙耳机控制?

A:只要正确接入 AVSession 并注册了播放/暂停/切歌指令监听,蓝牙耳机控制会自动生效。系统会将蓝牙指令转发到 AVSession 的对应回调中。

Q4:播放网络音频时如何优化起播速度?

A:使用 AVPlayer 的 setBufferingStrategy 设置预加载策略,或采用 HLS/DASH 协议实现自适应码率。同时确保网络请求使用合适的缓存策略。

Q5:多个页面如何共享同一个播放器实例?

A:使用单例模式(如本文的 MusicPlayer.getInstance()),在应用启动时初始化,所有页面通过同一个实例控制播放。避免每个页面独立创建 AVPlayer 造成资源冲突。


十、总结

HarmonyOS 的后台音乐播放需要 AVSession、长时任务和音频焦点三大能力协同工作。本文从架构设计到代码实现,系统梳理了完整的后台音频播放方案。核心设计要点总结如下:

  1. AVSession 是播控中心与应用的桥梁:负责向系统注册媒体信息、接收播控指令、同步播放状态。
  2. 长时任务是后台保活的通行证AUDIO_PLAYBACK 类型长时任务确保应用退后台后不被系统回收。
  3. 音频焦点是多应用共存的基础:通过合适的并发模式,确保音乐播放器与电话、导航、短视频等应用和平共处。
  4. 状态同步是用户体验的关键:应用必须及时响应播控指令并同步状态,避免"假播放"现象。
  5. 生命周期管理决定资源消耗:播放时申请、暂停时释放、恢复时重新申请,避免长时任务空转。
  6. 恢复播放需要手动三步重连:收到 RESUME 事件后,重设场景、重新激活、启动渲染器,缺一不可。

希望本文能帮助开发者在 HarmonyOS 应用中构建稳定、流畅、用户友好的后台音乐播放系统。


转载自:https://blog.csdn.net/u014727709/article/details/163730136
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

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

更多推荐