React Native:如何加载并播放音频

在 React Native 和 Expo AV 中处理音视剪辑

Expo AV:可靠的原生音视频支持

本文介绍了 expo-av 包,即一个用于 React Native 项目的通用音频(播放和录制)和视频模块。该包由Expo维护,是引导平台一系列 React Native 项目的首选库。

在React Native生态系统中,音频包更新很快,其中大部分要么不工作要么没有及时更新。在寻找要采用的软件包时——尤其是对于音频播放等关键任务——需要一个可靠且维护良好的软件包。每年 iOS 都会发布重要更新,而 Android 更新的频率更高。这些更新有时会带来重大更改,因此在 React Native 端选择可靠的源代码符合每个开发人员的利益。

在 React Native 和 Expo AV 中处理音视剪辑
注意, react-native-audio 超过2年没有更新,并且 react-native-sound 已经停滞超过1年(从撰写本文开始);所以应避免使用这些包,因为它们不支持本机最新 API,并且还会使项目中的故障变得很棘手。

话虽这么说, expo-av 在维护和支持平台也起着不小的作用。每周下载量超过 25,000 次,最后一次更新发布于一个月前(在撰写本文时),它是一个被广泛采用的软件包,开发人员对此很有信心。并非所有App都需要音频,因此很难衡量整个 React Native 生态系统的受欢迎程度。

`

expo-av 也有强大的文档,可以保持最新状态。读者可以访问GitHub 上的包作为真正的来源,但大部分文档托管在 Expo 网站上,其中有一个专门的音频页面以及Expo AV 模块的API 参考页面。在构建一些音频工具时,我们将在本文中引用该文档。

本文将涵盖的内容

既然我们已经了解到Expo AV 是使用 React Native 音频的方式,现在是时候开始进行深入的研究了。我们不会一味介绍加载并播放音视频剪辑的标准教程,而是用真实案例使介绍变得更有趣。

安装完成后,我们会了解:

  • 解释音频加载和播放工作流程以及使音频正常工作所需的关键 API。

  • 浏览加载、播放、重置和停止音频剪辑的 音频控制器 类。这个类将与 React 组件分开,以便将音频逻辑与组件逻辑分开。

  • 演示如何同时加载和管理多个音频剪辑。对于这件作品,我们假设需要分别加载 男性女性 音频剪辑,最终用户可以自行选择要收听的性别。当然,切换性别可以通过状态管理简单地切换性别来实现,但相应的音频本身必须加载并可以播放。

  • 漫游一个 <PlayButton /> 组件,它将 初始化音频 (请求从远程服务器加载音频文件),并管理音频状态。更具体地说,我们将访问诸如 自动播放点击播放点击停止等概念 ,并将其反映在组件状态中。另一件需要使用音频管理的事 是检索 音频时的组件状态。这意味着异步请求音频并等待它完全加载才能播放。这进一步(但需要)增加了组件的复杂性,以免尝试播放会导致运行时错误的未加载音频文件。

GitHub Gists 将贯穿整篇文章以演示这些概念。让我们开始介绍一些音频工作所需的关键 API expo-av

Expo AV 入门

expo-av 可以使用 expo 或 yarn 安装:

expo install expo-av
#or
yarn add expo-av

我们将从包中导入的唯一对象是 Audio ,它将处理我们将要下载和播放的所有音频剪辑:

import { Audio } from 'expo-av'

在这里, Sound 可以初始化对象。Sound 对象公开了加载和卸载声音、 Sound 通过事件侦听器侦听状态更改等所需的所有必要方法。初始化 Sound 对象可以作为变量、状态甚至类属性来完成:

// initialising Sound as a variable
const audioClip = new Audio.Sound();
// as component state
const [audioClip] = useState(new Audio.Sound());
// or as a class property (recommended)
class SomeClass {
   audioClip = new Audio.Sound();
   ...
}

有了 Sound 对象初始化,它就有可能:

  • 使用 loadAsync() 方法从外部源加载音频文件。当我们处理完音频时,该 unloadAsync() 方法会删除音频文件——例如,可以在包含音频文件的组件被卸载时调用。

  • 加载音视频文件后,可以使用 playAsync()replayAsync() 方法播放或重放。注意, playAsync() 如果音频文件尚未播放,它将播放音频文件,而即使该音频通过其时间线或已结束, replayAsync() 也会开始播放音频。

  • 获取有问题声音的当前状态,使用 getStatusAsync() 方法。这将返回一个带有元数据的对象,例如 isLoaded , 以确定我们是否可以正常播放音频剪辑。

  • 监听状态更新并通过事件侦听器对它们做出反应。为此,使用了 setOnPlaybackStatusUpdate(({ shouldPlay, isLoaded }) => { ... }) 事件侦听器。此功能是演示中的一个关键组件,我们可以进一步了解。

##m音频来源:本地或远程

expo-av 的主要限制是我们不能在设备上永久保存音频文件,例如缓存或某些键值系统中的 AsyncStorage 。 相反,我们被限制在本地捆绑的音频(对动态内容不是很有用),或者远程访问音频。

因此,对于无法捆绑到最终App构建中的动态内容,这需要 我们从外部服务器加载音频剪辑——也许是你自己的 Node 服务器为你的App服务端点。这无疑会带来负面影响,例如需要互联网连接来流式传输相关音频剪辑,并且必须为音频状态转换(加载音频、准备播放和加载失败)提供实时 UX。但这是我们目前必须考虑解决的一个限制。

话已至此,下一节将展示一个 Controller 类,该类将使用上述一些方法同时管理两个音频剪辑。

控制器类演练

以下 Controller 类的想法可以在 React 组件中利用简化对象管理多个音频剪辑(使用上面讨论的 API)。一个 <PlayButton /> 组件将依赖 Controller 类。

这里的控制器同时管理两个音频文件,分别标记为 audioFemaleaudioMale 。此设置假设了一个虚拟助手场景,用户可以在其中切换性别:- 男性和女性音频都将被加载并可供播放,在切换过程中的消除任何其他加载状态。毫无疑问,还有其他方式可以利用并发的音频管理,这取决于你如何提供动态内容。

Controller 类很简单,并且被嵌入Gist中。该类包含以下方法:

  • loadClips(token, uriFemale, uriMale) :用于 Sound.loadAsync() 联系提供相关音频文件的 API 端点。虽然 loadAsync() 不支持请求正文,但它支持请求标头,可用于存储身份验证令牌。

  • playAudio(gender) :在相应的音频文件调用 Sound.replayAsync()

  • stopAudio() :调用所有音频剪辑 Sound.stopAsync()

最后两种方法处理卸载有问题的音视频文件。一个阻塞执行(用于 await 暂停包含函数的执行,直到卸载任务被解决),并且执行比另一个更多的音频状态检查:

  • resetAudioClips() :调用所有音频剪辑 Sound.unloadAsync() ,不阻止执行。
  • cautiousResetAudioClips() : 调用 Sound.unloadAsync() ,阻止进一步执行,直到卸载函数得到解决。除此之外,还会检查音频文件的状态(通过 Sound.getStatusAsync() )以检查它们是否已经加载了音频。

如果下一个组件状态依赖于那些正在卸载的声音文件,而并发操作或状态更新依赖于这些卸载来解决,则应该使用 cautiousResetAudioClips() 。但是,如果卸载音频没有直接影响,则使用 resetAudioClips() 。在后一种情况下,包含函数将不会等待解析,而是会继续执行其逻辑。

如果音频加载或卸载出现错误,React Native 会标记警告 - 例如,如果在卸载包含组件后加载音频文件。此类事件虽不会使你的App崩溃,但应尽可能避免。

了解 Controller 的作用后,以下 Gist 包含整个类:



读者可以调整方法以适合他们的用例,例如 apiEndpoint 类属性。

值得指出此实现中的一些细节:

  • Sound.loadAsync() , 可以在其 uri 参数中采用通用 URL,该 URL不直接指向相关音频文件。这允许你只服务一个端点来处理 所有 音频文件请求,并使用请求头支持来包含音频文件的路径。然后,你的端点可以检查此文件是否存在,并通过身份验证执行其他检查。

  • 此外 Sound.loadAsync() ,还提供了 shouldPlayvolume 属性。 shouldPlay 提示 API 在音频准备好后立即开始播放,而音量则更详细地说明音频应该有多大(相对于设备音量)。

  • 注意, cautiousResetAudioClips() 会增加其所有检查的复杂性,并且可能会在等待每个承诺解决时显示降低App响应速度。在使用此功能和测试你的App性能时请记住这一点。

Controller 可以在任何 React 组件中实例化,并相应地用于任何一对音频剪辑。

下一节将所有这些逻辑放在一个 <PlayButton /> 组件中。它是在该组件中定义和管理上述事件侦听器,允许组件对音频状态更新做出反应,例如更新其本地状态以反映该状态。

PlayButton 组件演练

<PlayButton /> 组件代表一个简单的按钮,在功能上将能够播放或停止当前活动的音频剪辑。按钮本身有 3 个主要状态:

  • 一个 加载状态 ,由此按钮无法与之交互。这将是初始状态,直到所有音频剪辑都已下载并准备好播放。
  • 一个 播放状态 ,其中轻点按钮将停止播放音频。
  • 一个 就绪状态 ,按钮用来播放音频。

A Gist containing the entire component is included after this section. Before that, key elements and design choices are discussed so the reader can gain intuition into how the component operates.

包含整个组件的 Gist 在本节之后会讲到。在此之前,我们讨论了关键元素和设计选择,以便读者可以直观地了解组件的运行方式。

如感兴趣,请参阅 此处 的要点 因为下一节将讨论其实现。

组件状态设计决策

Controller 我们上面定义的类被初始化为组件状态变量,确保组件的重新渲染不会重置我们的音频状态——组件确实​​会频繁地重新渲染以反映音频状态的变化。

在导入阶段, Controller 为了进一步明确它与音频相关而重命名:

import { Controller as AudioController } from './Controller'

并且嵌入在组件状态中:

state = {
   audioController: new AudioController(),
   audioFemaleReady: false,
   audioMaleReady: false,
   audioPlaying: false,
   autoPlayed: false
}

注意,该组件会跟踪关键音频状态,包括剪辑是否已准备好播放、音频是否正在播放以及是否已触发自动播放。此处的任何更改都会触发重新渲染。

我对 Expo AV 对类组件和功能组件的详尽测试得出的结论是,类组件更适合管理音频。当定义组件生命周期方法时,这会变得有些明显,它们能有效组织与组件生命周期相关的音频生命周期。

生命周期方法及其音频管理角色

组件生命周期方法是管理音频的 关键 。一旦组件安装好,就需要初始化音频剪辑。这是在一个单独的异步函数中完成的 initAudioClips

componentDidMount () {
   this.initAudioClips();
}
async initAudioClips () {
   // initiate audio clips and event listeners
   // (explored further down)
}

componentDidUpdate(prevProps) 组件的生命周期方法是在触发重新渲染-或每当状态与部件的变化时建立的。这里需要检查的是音频片段本身是否发生了变化,或者用户是否交换了音频片段(在这种情况下,更改了性别)。这仅与使用当前道具检查先前渲染的道具有关:

componentDidUpdate (prevProps) {
   // if audio has changed, unmount the current audio clip and trigger reset
    if (prevProps.audioMale !== this.props.audioMale) {
      this.handleReplaceAudio()
    }
   // handle gender change
   if (prevProps.audioGender !== this.props.audioGender) {
      this.handleGenderChange();
    }
  }

卸载阶段也起着关键作用,任何加载的音频都需要卸载,并删除它们的事件侦听器。这可以在 componentWillUnmount 中看到:

componentWillUnmount () {
  const { audioController } = this.state;
  
  // remove status update event listeners
  audioController.audioFemale.setOnPlaybackStatusUpdate(null);
  audioController.audioMale.setOnPlaybackStatusUpdate(null);
  
  // reset (unload) audio clips
  audioController.cautiousResetAudioClips();
}

一个 handleReplaceAudio 函数的行为用与 componentWillUnmount 类似的方式,但更新了组件状态完全复位的音频状态:

image

当获取新的音频文件(并作为组件道具传递)时调用此函数,因此调用 initAudioClips 以使用其事件侦听器重新加载它们。 initAudioClips 是定义音频状态更新逻辑的地方。

启动音频剪辑及其事件侦听器

如果你参考具体的实践操作,需注意到 initAudioClips 做了两件事:

  • 通过 audioController.loadClips() 加载音频文件。
  • 为每个音频剪辑定义了两个事件侦听器,包含的逻辑几乎相同。

注意,initAudioClips 会在组件安装后以及音频文件发生更改时(与组件道具更改有关)被调用。每次发生音频状态更新时,正是此函数触发音频请求并更新组件状态。

加载音频剪辑非常简单,因为我们已经在 Controller 类中定义了逻辑。在定义事件侦听器之前,组件必须处理完这一步:

// await clips to load (from external endpoint)
await this.state.audioController.loadClips(authToken, uriFemale, uriMale);

现在,对于 audioController.audioFemaleaudioController.audioMale ,它们的事件侦听器已被定义好。这是第一个的部分——注意我们可以直接访问 shouldPlay 和 isLoaded,都由 Expo AV 直接提供:

this.state.audioController.audioFemale.setOnPlaybackStatusUpdate(({ shouldPlay, isLoaded }) => {
   // handle any status updates
   ...
}

这些事件侦听器中的逻辑相对简单易读。此处程序函数具体如下:

  • 如果 shouldPlaytrue 则更新状态值 a u dioPlaying 。如果 s houldPlay 已经匹配 state.audioPlaying ,React 将忽略这一步。
  • 如果 isLoadedtruestate.audio<gender>Ready 则将更新为 true 。这允许按钮从 加载状态 更改为 就绪状态
  • 如果 autoPlay 打开(通过一些全局状态管理),并且音频还没有自动播放,那么音频开始播放 audioController.playAsync() ,另一个状态更新恰好反映了自动播放的发生(我们不希望连续自动播放和重新渲染循环!)。

注意,整个自动播放逻辑包含在 setTimeout 700 毫秒的语句中。这纯粹是为了用户体验,所以按钮会及时更新到 就绪状态 ,然后再到 播放状态 ,以便用户确认。

如前所述,此逻辑是为每个音视频剪辑定义的,因为它们都是同时处理的。

渲染组件

该函数的其余部分处理是渲染按钮本身。这不是本文的重点,但读者可以依赖 React Native Button 或创建自己的 UI。

Material UI 包含一些非常有用的 Button 组件,它们支持开箱即用的图标、标签和关键功能。要集成 Material UI 按钮与你的音频控制器,请参阅我的文章: React: Theming with Material UI

要遵循的要点包含整个 <PlayButton /> 组件实现:



image

总结

这篇文章向读者介绍了 Expo AV,这是目前最好的音频、视频和音视频录制的 React Native 解决方案。介绍了 Expo AV API 及其关键功能。在此之后,我们介绍了一个用于管理多个音频剪辑的综合解决方案,它将 Controller 类和 <PlayButton /> 组件之间的逻辑分开。

这种控制器/组件方法在 API 的实验中效果最好。将 expo-av 方法包装在更简单的类定义中,可以在组件端更加简化音频剪辑的管理。组件可以专注于状态更新、事件侦听器逻辑和管理组件生命周期,而控制器可以处理音频请求和音频控制,例如播放和停止音频。

希望读者现在可以在自己的项目中应用或调整这些解决方案!

Expo AV 文档可以在他们的 Audio 页面 AV API Reference 中找到 。控制器类在 这里 PLAYBUTTON组件 在这里

原文作者 Ross Bulat
原文链接 https://rossbulat.medium.com/react-native-how-to-load-and-play-audio-241808f97f61

推荐阅读
作者信息
AgoraTechnicalTeam
TA 暂未填写个人简介
文章
169
相关专栏
本专栏仅用于分享音视频相关的技术文章,与其他开发者和 Agora 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。