【HarmonyOS】使用AVPlayer播放音乐,导致系统其它应用音乐播放暂停 - 播放音频焦点管理

【HarmonyOS】使用AVPlayer播放音乐,导致系统其它应用音乐播放暂停 - 播放音频焦点管理

一、前言

在鸿蒙系统中,对于音乐播放分为几种场景。音乐,电影,音效,闹钟等。当使用AVPlayer播放音乐时,如果不处理播放焦点模式,默认会交给系统处理。系统处理多个音乐播放时,会按照触发顺序依次暂停当前,再继续下一个。

例如当华为音乐应用正在播放音乐,此时你的应用使用AVPlayer进行音乐播放,就会导致华为音乐播放暂停,开始播放你的音乐。如果你的是音乐应用,默认这样处理是OK的。但是如果你使用AVPlayer播放一个短时音乐或者音效。那这样处理就不好了。这个问题实际上是播放焦点管理,如果不管理就会造成冲突。

此时我们的预期可以是,播放完短时音乐或者音效后,继续播放华为音乐 。或者当我们播放短时音乐或者音效时,声音大。将华为音乐播放声音降低 。亦或者是,两个同时播放

不同的业务场景,需求不同。根据应用产品调性来决定播放处理模式。

二、如何解决播放焦点冲突?

这需要根据你的应用场景来决定。

1.SoundPool 音频池 【完整代码参见章节三】

当你的应用只是播放短时音乐或者音效,那可以不使用AVPlayer,使用SoundPool音频池来处理该场景。效果就是同时播放音乐。你的应用音效播放,并不会干扰其他应用音乐的播放。

只需要设置SoundPool的AudioRendererInfo,配置usage字段为STREAM_USAGE_UNKNOWN,

STREAM_USAGE_MUSIC,

STREAM_USAGE_MOVIE,

STREAM_USAGE_AUDIOBOOK时,为混音模式,不会打断其他音频播放。

dart 复制代码
    let audioRendererInfo: audio.AudioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
      rendererFlags: 1
    }
   let soundPool: media.SoundPool = await media.createSoundPool(5, audioRendererInfo);

SoundPool的使用极其简单,只需要关心音频资源的获取,拿到SoundPool实例后,设置播放的参数,例如播放次数,音量,优先级等。将音频资源赋值后,就可以进行播放。

dart 复制代码
   public async PlaySoundPool() {
    // 开始播放,这边play也可带播放播放的参数PlayParameters,请在音频资源加载完毕,即收到loadComplete回调之后再执行play操作
     this.soundPool?.play(this.soundId, this.playParameters, (error, streamID: number) => {
      if (error) {
        console.info(this.TAG,`play sound Error: errCode is ${error.code}, errMessage is ${error.message}`)
      } else {
        this.streamId = streamID;
        console.info(this.TAG, 'play success streamID:' + streamID);
      }
    });
     // 设置声道播放音量
     await this.soundPool?.setVolume(this.streamId, 1, 1);
    // 设置循环播放次数
    await this.soundPool?.setLoop(this.streamId, 3); // 播放3次
    // 设置对应流的优先级
    await this.soundPool?.setPriority(this.streamId, 1);
  }

2.播放焦点管理 AudioSessionManager 【完整代码参见章节三】

当你的应用是播放音乐时,需要设置音频会话管理的模式,来设置兼容同时播放,还是暂停其他,优先播放当前。亦或者是播放自己的声音大,其他声音小。

如上图可见,AudioSessionManager的创建实际上相当于对播放AVplayer的初始化和播放控制做了一层包裹。包裹之后就可以针对播放进行播放音频焦点的管理。管理具体的播放模式,并发播放,优先播放,声音大播放等。

AudioSessionManager的使用也很简单,只需要拿到实例后,开启会话和关闭会话两个处理即可。开始会话时,需要配置音频焦点模式。

Strategy - concurrencyMode 共有四种模式:

默认模式(CONCURRENCY_DEFAULT ):即系统默认的音频焦点策略。

并发模式(CONCURRENCY_MIX_WITH_OTHERS ):和其它音频流并发。

降低音量模式(CONCURRENCY_DUCK_OTHERS ):和其他音频流并发,并且降低其他音频流的音量。

暂停模式(CONCURRENCY_PAUSE_OTHERS):暂停其他音频流,待释放焦点后通知其他音频流恢复。

dart 复制代码
  private mAudioSessionManager: audio.AudioSessionManager | null = null;
  private mStrategy: audio.AudioSessionStrategy | null = null;

   private initAudioSession(){
    let audioManager = audio.getAudioManager();
    this.mAudioSessionManager = audioManager.getSessionManager();
    this.mStrategy = {
      // 和其它音频流并发。
      concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_MIX_WITH_OTHERS
    };
    this.mAudioSessionManager.on('audioSessionDeactivated', (audioSessionDeactivatedEvent: audio.AudioSessionDeactivatedEvent) => {
      console.info(this.TAG,`reason of audioSessionDeactivated: ${audioSessionDeactivatedEvent.reason} `);
    });
  }

在播放音乐前,需要调用开始会话。才能生效音频管理的焦点模式:

如果不做其他处理,在音乐播放完后,一分钟后会话会自动关闭。

dart 复制代码
    // 设置并发播放音频
    await this.mAudioSessionManager?.activateAudioSession(this.mStrategy);
    let isActivated = this.mAudioSessionManager?.isAudioSessionActivated();
    console.log(this.TAG, "play isActivated: " + isActivated);

你也可以在音乐结束后,手动结束会话:

dart 复制代码
    await this.mAudioSessionManager?.deactivateAudioSession();

三、源码示例:

SoundPool 实现短时音乐或者音效播放

SoundPoolMgr.ets

dart 复制代码
import { audio } from '@kit.AudioKit';
import { media } from '@kit.MediaKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { resourceManager } from '@kit.LocalizationKit';

export class SoundPoolMgr {
  private TAG: string = 'SoundPoolMgr';

  // 单例对象
  private static mSoundPoolMgr: SoundPoolMgr | null = null;

  // 创建单例
  public static Ins(): SoundPoolMgr{
    if(!SoundPoolMgr.mSoundPoolMgr){
      SoundPoolMgr.mSoundPoolMgr = new SoundPoolMgr();
    }
    return SoundPoolMgr.mSoundPoolMgr;
  }

  private soundPool: media.SoundPool | null = null;
  private streamId: number = 0;
  private soundId: number = 0;
  private playParameters: media.PlayParameters | null = null;

  public async init(){
    // audioRenderInfo中的参数usage取值为STREAM_USAGE_UNKNOWN,STREAM_USAGE_MUSIC,STREAM_USAGE_MOVIE,
    // STREAM_USAGE_AUDIOBOOK时,SoundPool播放短音时为混音模式,不会打断其他音频播放。
    let audioRendererInfo: audio.AudioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
      rendererFlags: 1
    }
    this.playParameters = {
      loop: 3, // 循环4次
      rate: audio.AudioRendererRate.RENDER_RATE_NORMAL, // 正常倍速
      leftVolume: 1, // range = 0.0-1.0
      rightVolume: 1, // range = 0.0-1.0
      priority: 0, // 最低优先级
    }

    //创建soundPool实例
    this.soundPool = await media.createSoundPool(5, audioRendererInfo);

    let uri: string = "";
    // // 加载音频资源
    // await fs.open('/test_01.mp3', fs.OpenMode.READ_ONLY).then((file: fs.File) => {
    //   console.info("file fd: " + file.fd);
    //   uri = 'fd://' + (file.fd).toString()
    // }); // '/test_01.mp3' 作为样例,使用时需要传入文件对应路径。
    // this.soundId = await this.soundPool.load(uri);
    try {
      let value: resourceManager.RawFileDescriptor = await getContext().resourceManager.getRawFd("test.mp3");
      let fd = value.fd;
      let offset = value.offset;
      let length = value.length;
      this.soundId = await this.soundPool.load(fd, offset, length);
    } catch (error) {
      let code = (error as BusinessError).code;
      let message = (error as BusinessError).message;
      console.error(this.TAG,`callback getRawFd failed, error code: ${code}, message: ${message}.`);
    }


    // 加载完成回调
    this.soundPool.on('loadComplete', (soundId_: number) => {
      console.info(this.TAG, 'loadComplete, soundId: ' + soundId_);
    })
    // 播放完成回调
    this.soundPool.on('playFinished', () => {
      console.info(this.TAG,"receive play finished message");
      // 可进行下次播放
    })
    //设置错误类型监听
    this.soundPool.on('error', (error: BusinessError) => {
      console.info(this.TAG, 'error happened,message is :' + error.message);
    })
  }

   public async PlaySoundPool() {
    // 开始播放,这边play也可带播放播放的参数PlayParameters,请在音频资源加载完毕,即收到loadComplete回调之后再执行play操作
     this.soundPool?.play(this.soundId, this.playParameters, (error, streamID: number) => {
      if (error) {
        console.info(this.TAG,`play sound Error: errCode is ${error.code}, errMessage is ${error.message}`)
      } else {
        this.streamId = streamID;
        console.info(this.TAG, 'play success streamID:' + streamID);
      }
    });
     // 设置声道播放音量
     await this.soundPool?.setVolume(this.streamId, 1, 1);
    // 设置循环播放次数
    await this.soundPool?.setLoop(this.streamId, 3); // 播放3次
    // 设置对应流的优先级
    await this.soundPool?.setPriority(this.streamId, 1);
    // 设置音量
    await this.soundPool?.setVolume(this.streamId, 0.5, 0.5);
  }

  public async release() {
    // 终止指定流的播放
    await this.soundPool?.stop(this.streamId);
    // 卸载音频资源
    await this.soundPool?.unload(this.streamId);
    //关闭监听
    this.soundPool?.off('loadComplete');
    this.soundPool?.off('playFinished');
    this.soundPool?.off('error');

    // 释放SoundPool
    await this.soundPool?.release();

  }

}

音效播放管理类 添加了AudioSessionManager 播放焦点管理逻辑

AudioMgr .ets

dart 复制代码
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { audio } from '@kit.AudioKit';

/**
 * 音效播放管理类
 */
export class AudioMgr {
  private TAG: string = 'AudioMgr';

  // 单例对象
  private static mAudioMgr: AudioMgr | null = null;

  // 播放器实例
  private mAVPlayer: media.AVPlayer | undefined = undefined;

  // 是否初始化
  private isInit: boolean = false;

  private mAudioSessionManager: audio.AudioSessionManager | null = null;
  private mStrategy: audio.AudioSessionStrategy | null = null;

  // 创建单例
  public static Ins(): AudioMgr{
    if(!AudioMgr.mAudioMgr){
      AudioMgr.mAudioMgr = new AudioMgr();
    }
    return AudioMgr.mAudioMgr;
  }

  /**
   * 初始化接口(可以提前初始化,也可以直接调用play接口,使用时初始化)
   */
  public async init() {
    console.log(this.TAG, "play init start");

    let audioManager = audio.getAudioManager();
    this.mAudioSessionManager = audioManager.getSessionManager();
    this.mStrategy = {
      // 和其它音频流并发。
      concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_MIX_WITH_OTHERS
    };
    this.mAudioSessionManager.on('audioSessionDeactivated', (audioSessionDeactivatedEvent: audio.AudioSessionDeactivatedEvent) => {
      console.info(this.TAG,`reason of audioSessionDeactivated: ${audioSessionDeactivatedEvent.reason} `);
    });

    // 创建avPlayer实例对象
    this.mAVPlayer = await media.createAVPlayer();
    // 创建状态机变化回调函数
    this.registerStateChange(this.mAVPlayer);
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程
    this.registerErrorCall(this.mAVPlayer);
    // 获取raw音效资源
    let fileDescriptor = await getContext(this).resourceManager.getRawFd("test.mp3");
    this.mAVPlayer.fdSrc = {
      fd: fileDescriptor.fd,
      offset: fileDescriptor.offset,
      length: fileDescriptor.length
    };
    this.isInit = true;
    console.log(this.TAG, "play init end");
    return this.mAVPlayer;
  }

  /**
   * 注册异常回调
   * @param avPlayer
   */
  private registerErrorCall(avPlayer: media.AVPlayer){
    avPlayer.on('error', (err: BusinessError) => {
      console.log(this.TAG, " err:" + JSON.stringify(err));
      // 调用reset重置资源,触发idle状态
      avPlayer.reset();
    })
  }

  /**
   * 注册状态变化回调
   * @param avPlayer
   */
  private registerStateChange(avPlayer: media.AVPlayer){
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {

      switch (state) {
      // 成功调用reset接口后触发该状态机上报
        case 'idle':
          console.info(this.TAG, 'stateChange idle-release');
          avPlayer.release(); // 调用release接口销毁实例对象
          break;

      // avplayer 设置播放源后触发该状态上报
        case 'initialized':
          console.info(this.TAG, 'stateChange initialized-prepare');
          avPlayer.prepare();
          break;

      // prepare调用成功后上报该状态机
        case 'prepared':
          console.info(this.TAG, 'stateChange prepared-setVolume');
          avPlayer.setVolume(1); // The value ranges from 0.00 to 1.00.
          break;

      // play成功调用后触发该状态机上报
        case 'playing':
          console.info(this.TAG, 'stateChange playing');
          break;

      // pause成功调用后触发该状态机上报
        case 'paused':
          console.info(this.TAG, 'stateChange paused');
          break;

      // 播放结束后触发该状态机上报
        case 'completed':
          console.info(this.TAG, 'stateChange completed');
          break;

      // stop接口成功调用后触发该状态机上报
        case 'stopped':
          console.info(this.TAG, 'stateChange stopped');
          // avPlayer.reset(); // 调用reset接口初始化avplayer状态
          break;

        case 'released':
          console.info(this.TAG, 'stateChange released');
          break;

        default:
          console.info(this.TAG, 'stateChange default');
          break;
      }
    });
  }

  /**
   * 播放音效
   */
  public async play(){
    console.log(this.TAG, "play isInit " + this.isInit);
    // 设置并发播放音频
    await this.mAudioSessionManager?.activateAudioSession(this.mStrategy);
    let isActivated = this.mAudioSessionManager?.isAudioSessionActivated();
    console.log(this.TAG, "play isActivated: " + isActivated);
    
    
    if(this.isInit){
      await this.mAVPlayer?.play();
    }else{
      console.log(this.TAG, "play play-init start");
      this.mAVPlayer = await this.init();
      console.log(this.TAG, "play play start");
      await this.mAVPlayer?.play();
      console.log(this.TAG, "play play end");
    }
  }

  /**
   * 销毁音效管理工具
   */
  public async destroy(){
    console.log(this.TAG, "play destroy start");


    await this.mAVPlayer?.release();
    this.mAVPlayer = undefined;
    this.isInit = false;
    AudioMgr.mAudioMgr = null;
    console.log(this.TAG, "play destroy end");

    await this.mAudioSessionManager?.deactivateAudioSession();
  }

}

注意
当应用通过AudioSession使用上述各种模式时,系统将尽量满足其焦点策略,但在所有场景下可能无法保证完全满足。
如使用CONCURRENCY_PAUSE_OTHERS模式时,Movie流申请音频焦点,如果Music流正在播放,则Music流会被暂停。但是如果VoiceCommunication流正在播放,则VoiceCommunication流不会被暂停。

相关推荐
电子科技圈9 分钟前
XMOS将在CES 2025上展出多款由边缘AI驱动的创新音效、音频、识别和处理解决方案
人工智能·科技·嵌入式硬件·mcu·物联网·音视频·iot
java156550579701 小时前
前端 mp4 视频改成 m3u8 流模式
前端·音视频
不是三毛没有半2 小时前
华为自反ACL实验
华为
yunmoon012 小时前
一个功能强大的视频翻译和本地化配音工具,支持影视级双语字幕/视频配音
音视频
Jack153027682792 小时前
D2761是为保护扬声器所设计的音频限幅器,其限幅值可通过外接电 阻来调节,适合在个人电脑、便携式音响等系统中作音频限幅用。
电脑·手机·笔记本电脑·音视频·音响
非ban必选2 小时前
JavaCV视频文件音视频分离
ffmpeg·音视频
wangjiecsdn5 小时前
uniapp打包APP跳转华为应用商城(跳转各个应用商店)
华为·uni-app
奇风8 小时前
鸿蒙技术分享:鸿蒙页面里更新卡片也太麻烦了吧!!!
harmonyos·鸿蒙应用开发
飞飞-躺着更舒服12 小时前
多媒体文件解复用(Demuxing)过程
计算机视觉·音视频
Forever_Hopeful12 小时前
华为HarmonyOS NEXT 原生应用开发:页面路由、页面和组件生命周期函数
华为·harmonyos