【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时,为混音模式,不会打断其他音频播放。

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

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

   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):暂停其他音频流,待释放焦点后通知其他音频流恢复。

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} `);});}

在播放音乐前,需要调用开始会话。才能生效音频管理的焦点模式:
如果不做其他处理,在音乐播放完后,一分钟后会话会自动关闭。

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

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

    await this.mAudioSessionManager?.deactivateAudioSession();

三、源码示例:

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

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.0rightVolume: 1, // range = 0.0-1.0priority: 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');// 释放SoundPoolawait this.soundPool?.release();}}

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

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流不会被暂停。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/63703.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux中inode、软硬连接

磁盘的空间管理 如何对磁盘空间进行管理? 假设在一块大小为500G的磁盘中,500*1024*1024524288000KB。在磁盘中,扇区是磁盘的基本单位(一般大小为512byte),而文件系统访问磁盘的基本单位是4KB,因…

基于卷积神经网络的垃圾分类系统实现(GUI应用)

1.摘要 本文主要实现了一个卷积神经网络模型进行垃圾图像分类,为了提高垃圾分类模型的准确率,使用使用Batch Normalization层、使用早期停止策略来防止过拟合等方法来优化模型,实验结果显示最终优化后的模型准确率较高90%左右。最终&#xf…

IDEA结合GitLab使用

GitLab新建仓库 使用管理员账号创建gitlab仓库创建空白文件填写项目名称及命名空间 注意:取消勾选【使用自述文件初始化仓库】,否则IDEA中push代码报错 设置仓库权限 【设置】-【仓库】-【受保护分支】中需要添加哪些角色可以提交与合并代码&#xff0…

洛谷 P1179 [NOIP2010 普及组] 数字统计 C语言

题目&#xff1a; https://www.luogu.com.cn/problem/P1179 思路&#xff1a;直接暴力过 代码&#xff1a; #include<iostream> using namespace std; int cnt(int x) {int sum 0;while(x){int temp x %10;if(temp 2){sum;}x x/10;}return sum; } int main(void) …

Android APP自学笔记

摘抄于大学期间记录在QQ空间的一篇自学笔记&#xff0c;当前清理空间&#xff0c;本来想直接删除掉的&#xff0c;但是感觉有些舍不得&#xff0c;因此先搬移过来。 Android导入已有外部数据库 2015.06.26在QQ空间记录&#xff1a;在Android中不能直接打开res aw目录中的数据…

在GitHub上fork 别人的仓库 到 自己的仓库,clone到本地,处理后再上传回Github请求PR的过程

如题 一、fork 别人的仓库 到 自己的仓库 这是一种完全的复刻&#xff0c;所有内容都会被拿过来。 点击fork 写信息 创建fork 二、把它clone 到本地 先回到home&#xff0c;打开刚才 我们fork 的工程。 复制地址。 然后 在我们 本地 你去创建一个文件夹 来 接受他 比如我…

MATLAB 识别色块和数量

文章目录 前言步骤 1: 读取图像步骤 2: 转换为 HSV 颜色空间步骤 3: 定义颜色范围步骤 4: 创建颜色掩码步骤 5: 应用形态学操作&#xff08;可选&#xff09;步骤 6: 标记和显示结果完整代码步骤七 返回色块坐标 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&…

深入理解偏向锁、轻量级锁、重量级锁

一、对象结构和锁状态 synchronized关键字是java中的内置锁实现&#xff0c;内置锁实际上就是个任意对象&#xff0c;其内存结构如下图所示 其中&#xff0c;Mark Word字段在64位虚拟机下占64bit长度&#xff0c;其结构如下所示 可以看到Mark Word字段有个很重要的作用就是记录…

文字稿 | MatrixOne2.0.0:AI向量与高可用能力的重磅升级MatrixOne 2.0.0 新特性解读

MatrixOne 2.0.0 是一款 AI 驱动的云原生超融合数据库&#xff0c;采用了存算分离的架构&#xff0c;全面优化了云上资源利用效率。 MatrixOne兼容 MySQL 协议和语法&#xff0c;具备支持混合负载场景的能力&#xff0c;并结合向量数据类型、全文检索等特性&#xff0c;为生成式…

Qt Xlsx安装教程

Qt Xlsx安装教程 安装perl 如果没有安装perl&#xff0c;请参考perl Window安装教程 下载QtXlsxWriter源码 下载地址 ming32-make编译32 lib库 C:\Qt\Qt5.12.12\5.12.12\mingw73_32>d: D:\>cd D:\Code\QtXlsxWriter-master\QtXlsxWriter-master D:\Code\QtXlsxWrit…

【49】AndroidStudio构建其他人开发的Android项目

(1)做Android软件开发&#xff0c;通常会看一些其他人开发的项目源码&#xff0c;当将这些项目的源码通过git clone到本地之后&#xff0c;用AndroidStudio进行打开时&#xff0c;通常会遇到一些环境配置的问题。本文即用来记录在构建他人开发项目源代码这一过程中遇到的一些常…

day08 接口测试(3)——postman工具使用

下载 postman 的历史版本&#xff1a;Postman 历史版本下载 - 简书 我自己根据我的电脑&#xff0c;安装的地址为&#xff1a;https://dl.pstmn.io/download/version/9.31.32/osx_64 今天开始学习 postman 这个测试工具啦。 【没有所谓的运气&#x1f36c;&#xff0c;只有绝…

OpenCV相机标定与3D重建(10)眼标定函数calibrateHandEye()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算手眼标定&#xff1a; g T c _{}^{g}\textrm{T}_c g​Tc​ cv::calibrateHandEye 是 OpenCV 中用于手眼标定的函数。该函数通过已知的机器人…

day08 接口测试(4)知识点完结!!

【没有所谓的运气&#x1f36c;&#xff0c;只有绝对的努力✊】 目录 1、postman读取外部数据文件&#xff08;参数化&#xff09; 1.1 数据文件简介 1.2 导入外部数据文件 1.2.1 csv文件 1.2.2 导入 json文件 1.3 读取数据文件数据 1.4 案例 1.5 生成测试报告 2、小…

linux 安装 Jenkins 教程

前言 Jenkins 是一个开源的自动化服务器&#xff0c;广泛用于持续集成&#xff08;Continuous Integration&#xff0c;CI&#xff09;和持续交付&#xff08;Continuous Delivery&#xff0c;CD&#xff09;领域。它帮助开发者自动化软件构建、测试、部署等过程&#xff0c;从…

IdentityServer4框架、ASP.NET core Identity

OAuth2.0 IdentityServer4 官网 中文官网 ASP.NET Core Identity提供了一个用来管理和存储用户账户的框架. IdentityServer4是基于ASP.NET Core实现的认证和授权框架&#xff0c;是对OpenID Connect和OAuth 2.0协议的实现。 IdentityServer是一个中间件,它可以添加符合OpenID…

ZZCMS2023存在跨站脚本漏洞(CNVD-2024-44822、CVE-2024-44818)

ZZCMS是一款用于搭建招商网站的CMS系统&#xff0c;由PHP语言开发&#xff0c;可快速搭建&#xff1a;医药招商、保健品招商、化妆品招商、农资招商、孕婴童招商、酒类副食类等招商网站。 国家信息安全漏洞共享平台于2024-11-14公布其存在跨站脚本漏洞。 漏洞编号&#xff1a…

使用Kubernetes部署MySQL+WordPress

目录 前提条件 部署MySQL和WordPress 编写yaml文件 应用yaml文件 存在问题及解决方案 创建PV(持久化卷) 创建一个PVC(持久化卷声明) 部署添加PVC 查看PV对应的主机存储 删除资源 查看资源 删除deployment和service 查看主机数据 删除PVC和PV 删除主机数据 前提条…

每日一刷——12.10——学习二叉树解题模式(二)

题目三&#xff1a;填充每个节点的下一个右侧节点指针1 题目描述&#xff1a;116. 填充每个节点的下一个右侧节点指针 - 力扣&#xff08;LeetCode&#xff09; 我的理解&#xff1a; 我的感觉是同父亲还好搞一点&#xff0c;感觉是在遍历到每一个节点的时候&#xff0c;就把…

Spring Cloud Alibaba:一站式微服务解决方案

Spring Cloud Alibaba介绍 在当今的软件开发领域&#xff0c;微服务架构因其灵活性、可扩展性和独立性等优势而备受青睐。Spring Cloud Alibaba 作为一款强大的一站式微服务解决方案&#xff0c;为开发者提供了丰富的工具和组件&#xff0c;帮助他们轻松构建和管理复杂的微服务…