鸿蒙NEXT开发【音频播放类应用交互场景实践】媒体开发

概述

对于音频播放类应用,除了歌曲播控的基础能力外,各种交互场景的设计也对用户体验有着重要的影响。本文以音乐播放器应用为例,从应用与用户、播放设备以及其他应用的交互三方面入手,分别对典型使用场景给出示例方案,为应用带来灵活多样、符合用户直觉的交互体验。

1

场景分析

根据当前HarmonyOS APP开发过程中遇到的实际音频类应用业务场景,总结提炼出如下典型场景,设计其交互功能与方案以供参考:

场景分类典型场景场景描述
与用户交互播控中心操控无需进入应用直接通过播控中心操控歌曲状态
后台播放音乐应用位于后台长时间持续播放音乐
与音频播放设备交互播放设备的状态发生改变新设备可用、旧设备不可用、用户切换设备场景的适配方案
响应播放设备的指令蓝牙耳机控制应用播放状态
用户主动切换播放设备通过投播组件选择播放设备
与其他应用交互与其他应用音频冲突被其他音频打断、与其他音频并发

与用户交互

播控中心控制音乐状态

在应用接入播控中心后,用户可以直接通过播控中心修改音频播放状态,如暂停/播放、下一首、切换播放模式、拖动进度条等,应用内的音频信息和状态也会同步显示在播控中心界面上,使得用户与应用的交互更加灵活易用。同时与播控中心交互使用的AVSession还管控着应用的后台播放能力,因此对于音乐类应用一般推荐接入播控中心

应用与播控中心交互的过程如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由图可见,应用的接入流程可以主要分为三个部分:创建AVSession、监听播控中心通知以及向播控中心上报应用状态,以下分别给出具体实现:

  1. 应用创建媒体会话

    在应用的音频渲染器初始化的时候创建并激活AVSession,将此会话用作应用与播控中心交互的媒介,音乐类应用选择创建audio类型的AVSession,不同类型的AVSession对应不同的播控中心样式

import { avSession } from '@kit.AVSessionKit';
// ...
export class AVSessionController {
  private context: common.UIAbilityContext | undefined = undefined;
  private AVSession: avSession.AVSession | undefined = undefined;
  private songList: SongItem[] = [];
  private musicIndex: number | undefined = undefined;
  private audioRendererController: AudioRendererController | undefined = undefined;
  // ...
  private async initAVSession() {
    this.context = AppStorage.get('context');
    if (!this.context) {
      Logger.info(TAG, `session create failed : conext is undefined`);
      return;
    }
    this.audioRendererController = AppStorage.get('audioRendererController');
    if (!this.audioRendererController) {
      Logger.info(TAG, `session create failed : audioRendererController is undefined`);
      return;
    }
    this.AVSession = await avSession.createAVSession(this.context, "PLAY_AUDIO", 'audio');
    await this.AVSession.activate();
    // ...
  }
  // ...
}
  1. 注册播控中心通知回调

    当用户在锁屏或者其他界面时希望通过播控中心控制应用的播放状态,比如播放、暂停、下一首等,应用需要设置监听回调来对播控中心的通知做出对应的调整,只有设置了回调,播控中心侧的按钮才会亮起来,否则按钮将会置灰。

// 监听播控中心的各种通知如播放、暂停、下一首、调整进度,调用应用对应的方法响应该通知
async setListenerForMesFromController() {
  if (!this.AVSession) {
    return;
  }
  this.AVSession.on('play', this.playCall);
  this.AVSession.on('pause', this.pauseCall);
  this.AVSession.on('playNext', this.playNextCall);
  this.AVSession.on('playPrevious', this.playPreviousCall);
  this.AVSession.on('seek', this.seekCall);
  this.AVSession.on('setLoopMode', this.setLoopModeCall);
  this.AVSession.on('toggleFavorite', this.toggleFavoriteCall);
}

适配时需要注意播控中心点击循环模式图标后,应用在回调中只会收到当前播控中心的图标状态,因此开发者需要在业务代码中自己管理状态切换顺序(如:单曲循环->顺序播放->随机播放->单曲循环),根据收到的当前状态指定要切换到的下一个状态。

// 响应播控中心点击循环模式图标通知
private setLoopModeCall: (loopMode: avSession.LoopMode) => void = (loopMode: avSession.LoopMode) => {
  Logger.info(TAG, `on loopMode , do loopMode task`);
  if (!this.audioRendererController) {
    Logger.error(TAG, 'audioRendererController is undefined in setLoopModeCall');
    return;
  }
  switch (loopMode) {
    case avSession.LoopMode.LOOP_MODE_SINGLE:
      this.audioRendererController.setPlayModel(MusicPlayMode.ORDER);
      break;
    case avSession.LoopMode.LOOP_MODE_SEQUENCE:
      this.audioRendererController.setPlayModel(MusicPlayMode.RANDOM);
      break;
    case avSession.LoopMode.LOOP_MODE_SHUFFLE:
      this.audioRendererController.setPlayModel(MusicPlayMode.SINGLE_CYCLE);
      break;
    default:
      break;
  }
  this.setPlayModeToAVSession();
  this.setPlayModeToControlArea();
}

应用如果已切换到后台,用户点击播控中心,将由播控中心负责拉起应用。应用需要配置参数来选择拉起的应用和页面,并通过setLaunchAbility()方法配置给媒体会话。

private setLaunchAbility() {
  if (!this.context) {
    return;
  }
  let wantAgentInfo: wantAgent.WantAgentInfo = {
    wants: [
      {
        bundleName: this.context.abilityInfo.bundleName,
        abilityName: this.context.abilityInfo.name
      }
    ],
    operationType: wantAgent.OperationType.START_ABILITIES,
    requestCode: 0,
    wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
  };
  wantAgent.getWantAgent(wantAgentInfo).then((agent) => {
    if (this.AVSession) {
      this.AVSession.setLaunchAbility(agent);
    }
  });
}
  1. 应用状态上报播控中心

    当应用内音频信息发生改变时,如用户手动在应用内操作或应用响应播控中心通知进行调整后,需要向播控中心上报应用中的音频状态数据,播控中心修改界面显示来达到播控中心与应用的状态同步,通过setAVPlaybackState()方法可以上报媒体播放状态,如暂停、播放、进度调整、循环模式、收藏状态等。

// 设置收藏状态
private setFavoriteState(isFavorite: boolean) {
  if (this.AVSession) {
    this.AVSession.setAVPlaybackState({ isFavorite }, (err: BusinessError) => {
      if (err) {
        Logger.error(TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
      } else {
        Logger.info(TAG, 'SetAVPlaybackState successfully');
      }
    });
  }
}

// 设置进度条状态
public setProgressState(ms: number) {
  if (this.AVSession) {
    this.AVSession.setAVPlaybackState({
      position: {
        elapsedTime: ms,
        updateTime: new Date().getTime()
      }
    }, (err: BusinessError) => {
      if (err) {
        Logger.error(TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
      } else {
        Logger.info(TAG, 'SetAVPlaybackState successfully');
      }
    });
  }
}

// 设置播放暂停状态
public setPlayState(isPlay: boolean) {
  if (this.AVSession) {
    this.AVSession.setAVPlaybackState({
      state: isPlay ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
    }, (err: BusinessError) => {
      if (err) {
        Logger.error(TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
      } else {
        Logger.info(TAG, 'SetAVPlaybackState successfully');
      }
    });
  }
}
// ...
// 设置循环模式
public async setLoopModeState(AVSessionLoopMode: avSession.LoopMode) {
  if (this.AVSession) {
    this.AVSession.setAVPlaybackState({ loopMode: AVSessionLoopMode }, (err: BusinessError) => {
      if (err) {
        Logger.error(TAG, `SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
      } else {
        Logger.info(TAG, 'SetAVPlaybackState successfully');
      }
    });
  }
}

通过setAVMetadata()方法可以上报媒体会话元数据如标题、歌手、歌词等。

public async setAVMetadata() {
  this.musicIndex = AppStorage.get('selectIndex');
  Logger.info(TAG, 'current musicIndex is:' + this.musicIndex);
  if (this.musicIndex === undefined) {
    this.musicIndex = 0;
  }
  try {
    if (this.context) {
      let mediaImage = await MediaTools.getPixelMapFromResource(this.context,
        this.songList[this.musicIndex].label as resourceManager.Resource);
      Logger.info(TAG, 'getPixelMapFromResource success' + JSON.stringify(mediaImage));
      let metadata: avSession.AVMetadata = {
        assetId: `${this.musicIndex}`,
        title: this.songList[this.musicIndex].title,
        artist: this.songList[this.musicIndex].singer,
        mediaImage: mediaImage,
        duration: this.getDuration(),
      };
      // 歌词文件需要解码lrc文件
      let lrc = await MediaTools.getLrcFromRawFile(this.context, this.songList[this.musicIndex].lyric);
      if (lrc) {
        metadata.lyric = lrc;
      }
      if (this.AVSession) {
        this.AVSession.setAVMetadata(metadata).then(() => {
          Logger.info(TAG, 'SetAVMetadata successfully');
        }).catch((err: BusinessError) => {
          Logger.error(TAG, `SetAVMetadata BusinessError: code: ${err.code}, message: ${err.message}`);
        });
      }
    }
  } catch (error) {
    Logger.error(TAG, `SetAVMetadata try: code: ${(error as BusinessError).code}`);
  }
}

注意此处上报歌词信息需要获取歌词lrc文件数据,解码成字符串后上传,具体处理如下:

import { util } from '@kit.ArkTS';
// ...
export class MediaTools {
  // ...
  static async getLrcFromRawFile(context: common.UIAbilityContext, filename: string): Promise<string | undefined> {
    if (!filename) {
      return undefined;
    }
    const lyricUint8Array: Uint8Array = await context.resourceManager.getRawFileContent(filename);
    let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
    let resStr: string = textDecoder.decodeToString(lyricUint8Array, { stream: false });
    return resStr;
  }
}

适配后效果如图,应用内的歌词、播放进度、收藏状态、循环模式、歌曲信息与播控中心展示状态一致:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

后台播放音乐

实现播控中心的接入后,解除了必须进入应用才能调整播放状态的限制,但应用退到后台状态后很快就会被挂起,导致歌曲中断。此时在接入AVSession的基础上还需要申请长时任务,保证歌曲能在后台一直保持播放状态,达到连贯的听歌体验,具体实现方式如下:

  1. 创建后台管理类,使用BackgroundTasksKit的startBackgroundRunning()/stopBackgroundRunning()方法分别申请和取消后台运行任务,长时任务类型选择AUDIO_PLAYBACK,表示音视频后台播放。
import { wantAgent, common } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from './Logger';

const TAG = 'BackgroundUtil';

export class BackgroundUtil {
  // 申请长时后台任务
  public static startContinuousTask(context?: common.UIAbilityContext): void {
    if (!context) {
      Logger.error(TAG, 'startContinuousTask failed', `context undefined`);
      return;
    }
    let wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: context.abilityInfo.bundleName,
          abilityName: context.abilityInfo.name
        }
      ],
      operationType: wantAgent.OperationType.START_ABILITY,
      requestCode: 0,
      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };

    wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: Object) => {
      try {
        backgroundTaskManager.startBackgroundRunning(context,
          backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, wantAgentObj).then(() => {
          Logger.info('this audioRenderer: ', 'startBackgroundRunning succeeded');
        }).catch((error: BusinessError) => {
          Logger.error('this audioRenderer: ', `startBackgroundRunning failed Cause: code ${error.code}`);
        });
      } catch (error) {
        Logger.error(TAG, `startBackgroundRunning failed.message ${(error as BusinessError).message}`);
      }
    });
  }

  // 停止长时后台任务
  public static stopContinuousTask(context: common.UIAbilityContext): void {
    try {
      backgroundTaskManager.stopBackgroundRunning(context).then(() => {
        Logger.info('this audioRenderer: ', 'stopBackgroundRunning succeeded');
      }).catch((error: BusinessError) => {
        Logger.error('this audioRenderer: ', `stopBackgroundRunning failed Cause: code ${error.code}`);
      });
    } catch (error) {
      Logger.error(TAG, `stopBackgroundRunning failed. message ${(error as BusinessError).message}`);
    }
  }
}
  1. 在音乐播放时申请长时任务,达到默认支持后台播放的效果。
// 选歌播放
async play(musicIndex: number = this.musicIndex) {
  if (!this.audioRenderer) {
    return;
  }
  if (musicIndex >= this.songList.length) {
    Logger.error(TAG, `current musicIndex ${musicIndex}`);
    return;
  }
  BackgroundUtil.startContinuousTask(this.context);
  this.updateMusicIndex(musicIndex);
  if (this.isFirst) {
    this.isFirst = false;
    await this.loadSongAssent();
    await this.stop();
    await this.start();
  } else {
    await this.stop();
    this.updateIsPlay(false);
    await this.reset();
  }
}
// ...

// 开始播放
public async start() {
  if (this.audioRenderer) {
    try {
      await this.audioRenderer.start().catch((err: BusinessError) => {
        Logger.error(TAG, `start failed,code is ${err.code},message is ${err.message}}`);
      })
      this.updateIsPlay(true);
      BackgroundUtil.startContinuousTask(this.context);
      Logger.info(TAG, 'start success');
    } catch (e) {
      Logger.error(TAG, `start failed,audioRenderer is undefined`);
    }
  }
}
  1. 在音频渲染器释放的时候取消长时任务。
// 释放audioRenderer
public async release() {
  if (this.audioRenderer && this.context) {
    try {
      await this.audioRenderer.release().catch((err: BusinessError) => {
        Logger.error(TAG, `release failed,code is ${err.code},message is ${err.message}}`);
      })
      this.avSessionController?.unregisterSessionListener();
      BackgroundUtil.stopContinuousTask(this.context);
      Logger.info(TAG, 'release success');
    } catch (e) {
      Logger.error(TAG, `release failed,audioRenderer is undefined`);
    }
  }
}

与播放设备交互

播放设备状态发生改变

当应用连接的播放设备状态发生改变时,需要及时做出对应的处理,从音频流输出目标的切换、音频播放状态的改变到应用界面的更新,都应该与系统行为以及用户直觉相符,来保证交互体验的一致性,以下分别对各种变更场景给出适配方案:

  1. 新设备可用

    新设备上线时,会触发播放设备自动切换,切换的优先级按下图所示,个人类设备高于公共类设备,同一优先级遵循后入优先原则。因此如果一开始通过扬声器播放,接入耳机后会自动切换至耳机播放,无需应用主动切换。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    应用可以通过监听outputDeviceChangeWithInfo事件来感知外放设备的切换,新设备上线对应的变更原因是AudioStreamDeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE,如上所述这种情况切换到新的设备播放的行为是系统行为,应用不需要主动改变输出设备;如果有特殊业务逻辑如接入新设备时暂停播放或者对新接入设备进行管理等,可在此类回调中进行处理。

  2. 旧设备不可用

    正在发声的设备不可用后(有线耳机断开、蓝牙开关关闭、蓝牙耳机入盒、多连接耳机被抢占等),应用需要根据使用场景选择暂停播放或使用新设备继续播放。在此列出一些常用类型场景的处理建议,应用可以根据需要做出调整。

    类型处理建议
    游戏场景不进行暂停
    音乐场景根据回调进行暂停
    听书场景根据回调进行暂停
    视频场景根据回调进行暂停

    应用同样可以监听设备变更事件并对AudioStreamDeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE的场景进行处理,在回调中根据当前播放设备类型选择后续行为,例如为扬声器时暂停,为其他设备则继续播放。

    注意具有佩戴检测能力的蓝牙耳机入耳、摘下时会通过播控中心分别对应用发送播放和暂停的通知,应用可以通过监听播控中心指令而非设备变更事件来适配此类操作。

  3. 用户强选

    当用户通过[投播组件]主动切换输出设备时,系统会自动切换到新选择的设备播放音频,应用可以对变更原因为AudioStreamDeviceChangeReason.REASON_OVERRODE的事件并处理自己的业务逻辑

示例代码如下,分别对各种类型的设备状态改变事件做出对应的处理:

private setOutputDeviceChangeCallback() {
  if (!this.audioRenderer) {
    return;
  }
  this.audioRenderer.on('outputDeviceChangeWithInfo', this.outputDeviceChangeCallback);
}

private outputDeviceChangeCallback: (deviceChangeInfo: audio.AudioStreamDeviceChangeInfo) => void =
  (deviceChangeInfo: audio.AudioStreamDeviceChangeInfo) => {
    Logger.info(TAG, `DeviceInfo id: ${deviceChangeInfo.devices[0].id}`);
    Logger.info(TAG, `DeviceInfo name: ${deviceChangeInfo.devices[0].name}`);
    Logger.info(TAG, `DeviceInfo address: ${deviceChangeInfo.devices[0].address}`);
    Logger.info(TAG, `Device change reason: ${deviceChangeInfo.changeReason}`);
    if (deviceChangeInfo.changeReason === audio.AudioStreamDeviceChangeReason.REASON_NEW_DEVICE_AVAILABLE) {
      // 新设备可用,应用按需适配业务逻辑
    } else if (deviceChangeInfo.changeReason === audio.AudioStreamDeviceChangeReason.REASON_OLD_DEVICE_UNAVAILABLE) {
      // 旧设备不可用,暂停播放
      Logger.info(TAG, `Device change reason: ${deviceChangeInfo.changeReason}`);
      this.pause();
    } else if (deviceChangeInfo.changeReason === audio.AudioStreamDeviceChangeReason.REASON_OVERRODE) {
      // 用户强选设备,应用按需适配业务逻辑
    }
  }

响应播放设备指令

通过有线耳机、蓝牙耳机实现对音频的播放、暂停、上一首、下一首等基本操作,需要应用接入播控中心,并注册播控命令事件监听,以便响应播控中心下发的命令。应用通过蓝牙耳机发送指令与直接点击播控中心控制音频机制相同,因此接入播控中心监听播控指令即可

用户主动切换播放设备

应用内可以提供主动切换播放设备的能力,来提升交互的灵活性,此时需要接入AVCastPicker组件,在用户界面点击此投播组件并在弹框中选择目标设备即可切换设备播放

import { AVCastPicker } from '@kit.AVSessionKit';
import { StyleConstants } from '../common/constants/StyleConstants';
import { BreakpointType } from '../common/utils/BreakpointSystem';

@Component
export struct TopAreaComponent {
  @StorageProp('currentBreakpoint') currentBreakpoint: string = 'sm';

  build() {
    Row() {
      AVCastPicker()
        .width(new BreakpointType({
          sm: $r('app.float.common_iamge'),
          md: $r('app.float.common_iamge'),
          lg: $r('app.float.control_image_lg')
        }).getValue(this.currentBreakpoint))
        .height(new BreakpointType({
          sm: $r('app.float.common_iamge'),
          md: $r('app.float.common_iamge'),
          lg: $r('app.float.control_image_lg')
        }).getValue(this.currentBreakpoint))
    }
    .height($r('app.float.info_margin_top_sm'))
    .width(StyleConstants.FULL_WIDTH)
    .justifyContent(FlexAlign.End)
  }
}

将投播组件布局在页面的合理位置,点击选择音频输出设备,如图所示可以从本机扬声器切换到已连接的蓝牙耳机:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

与其他应用交互

与其他应用音频冲突

当与其他应用的音频发生冲突,多个音频流同时播放时,系统预设了音频打断策略,对多音频的并发进行管控,只有持有音频焦点的音频流才可以正常播放,避免多个音频流无序并发播放的现象出现。为了维持应用和系统的状态一致性,保证符合用户直觉的交互体验,推荐应用[监听音频打断事件],并在收到音频打断事件([InterruptEvent])时做出相应处理。对于音乐类应用,被各种类型的其他应用音频打断场景和效果如下表所示:

先播应用音频类型后播应用音频类型打断效果
音乐闹钟后播应用播放时,先播应用暂停播放;后播应用停止播放后,先播应用恢复播放
电话
铃声
VOIP 铃声(全屏呼叫/呼叫页面/横幅呼叫)
VOIP 通话
VOIP MESSAGE(微信语音/畅联)
导航后播应用播放时,先播应用降低音量持续播放;后播应用停止播放后,先播应用恢复音量继续播放
TextReader控件朗读语音
语音助手类短语音
音乐后播应用播放时,先播应用停止播放;后播应用停止播放后,先播应用不再恢复播放
视频
游戏先播、后播应用并发混音播放
系统音效(锁屏/按键)

如表所示,根据应用打断效果主要可以分为四个类型:暂停后恢复、降低音量后恢复、停止后不恢复和并发播放,以下分别对这四类场景给出适配方案:

  1. 暂停后恢复场景

    后播音频类型为闹钟/电话/铃声/VOIP音频时,先播音频暂停播放;待后播音频播放完毕后,先播音频恢复播放。应用可以注册焦点事件监听,当此类打断事件发生时,系统会自动暂停先播音频,应用侧会接收到INTERRUPT_HINT_PAUSE事件,此时只需更新播放和UI界面状态为暂停态即可,不需要主动停止音频流。当后播音频结束后,应用接收到INTERRUPT_HINT_RESUME事件,此时系统不会主动继续播放先播音频,应用需主动调用播放方法续播音频流。

  2. 降低音量后恢复场景

    当后播应用音频类型为导航/TextReader语音/语音助手类语音播报时,先播音频应降低音量持续播放;后播音频播报结束后,先播音频恢复音量继续播放。注意此类打断事件的音量降低/恢复行为是系统行为,应用无需主动调整音量。在降低音量和恢复时会分别收到INTERRUPT_HINT_DUCK和INTERRUPT_HINT_UNDUCK回调,应用可以在回调中按需更新页面状态或处理其他业务逻辑。

  3. 停止后不恢复场景

    当后播应用音频类型为音乐/视频时,音乐终止播放;后播音频停止后,音乐不恢复播放。应用可以注册焦点事件监听,接收到INTERRUPT_HINT_STOP事件时,停止音乐播放,并更新UI界面。

  4. 并发场景

    当后播应用音频类型为游戏或系统提示音(锁屏、按键)时,先播音频不停止,与后播音频混音并发播放。此行为是系统默认行为,应用侧不需要适配。

    如果是音乐音频于后台播放,前台音视频静音并发播放的场景,需要前台应用使用setSilentModeAndMixWithOthers()接口开启静音并发模式,否则系统会根据音频流类型默认触发对应打断场景。

综上四种情况,在audioRenderer.on(‘audioInterrupt’)回调中分别对各种打断类型场景做出对应处理,并实现静音播放接口,示例代码实现如下:

// 监听音频打断事件
private setInterruptCallback() {
  if (!this.audioRenderer) {
    return;
  }
  this.audioRenderer.on('audioInterrupt', this.interruptCallback);
}

// 在回调中处理各类打断事件
private interruptCallback: (interruptEvent: audio.InterruptEvent) => void =
  (interruptEvent: audio.InterruptEvent) => {
    if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
      switch (interruptEvent.hintType) {
        case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
          // 此分支表示系统已将音频流暂停(临时失去焦点),为保持状态一致,应用需切换至音频暂停状态
          // 临时失去焦点:待其他音频流释放音频焦点后,本音频流会收到resume对应的音频打断事件,到时可自行继续播放
          this.updateIsPlay(false);
          break;
        case audio.InterruptHint.INTERRUPT_HINT_STOP:
          // 此分支表示系统已将音频流停止(永久失去焦点),为保持状态一致,应用需切换至音频暂停状态
          // 永久失去焦点:后续不会再收到任何音频打断事件,若想恢复播放,需要用户主动触发。
          this.updateIsPlay(false);
          this.pause();
          break;
        case audio.InterruptHint.INTERRUPT_HINT_DUCK:
          // 此分支表示系统已将音频音量降低(默认降到正常音量的20%)
          // 此时应用可以处理当音量降低时的业务逻辑
          break;
        case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
          // 此分支表示系统已将音频音量恢复正常
          // 此时应用可以处理当音量恢复时的业务逻辑
          break;
        default:
          break;
      }
    } else if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_SHARE) {
      switch (interruptEvent.hintType) {
        case audio.InterruptHint.INTERRUPT_HINT_RESUME:
          // 此分支表示临时失去焦点后被暂停的音频流此时可以继续播放,建议应用继续播放,切换至音频播放状态
          // 若应用此时不想继续播放,可以忽略此音频打断事件,不进行处理即可
          this.start();
          break;
        default:
          break;
      }
    }
  }
// ...
// 通过用户首选项数据设置静音并发模式
private async setSilentModeAndMixWithOthersFromPrefer() {
  if (!this.audioRenderer || !this.context) {
    return;
  }
  let formIds: string[] = await PreferencesUtil.getInstance().getFormIds(this.context);
  let isSupportSilent = false;
  if (formIds.includes(SILENT_ID)) {
    isSupportSilent = true;
  } else {
    isSupportSilent = false;
  }
  AppStorage.setOrCreate('isSilentMode', isSupportSilent);
  this.audioRenderer.setSilentModeAndMixWithOthers(isSupportSilent);
}

// 通过按钮设置静音并发模式
public async setSilentModeAndMixWithOthers(isSupportSilent: boolean = false) {
  if (!this.audioRenderer || !this.context) {
    return;
  }
  this.audioRenderer.setSilentModeAndMixWithOthers(isSupportSilent);
  let formIds: string[] = await PreferencesUtil.getInstance().getFormIds(this.context);
  // 存储静音状态至用户首选项
  if (isSupportSilent) {
    if (!formIds.includes(SILENT_ID)) {
      await PreferencesUtil.getInstance().addFormId(this.context, SILENT_ID);
    }
  } else {
    if (formIds.includes(SILENT_ID)) {
      await PreferencesUtil.getInstance().removeFormId(this.context, SILENT_ID);
    }
  }
  AppStorage.setOrCreate('isSilentMode', isSupportSilent);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值