npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

expo-audio-hooks

v1.0.3

Published

React hooks for expo-av audio

Downloads

14

Readme

expo-audio-hooks

React hooks for expo-av audio.

Installation

yarn add expo-av expo-audio-hooks

expo-av is a peer dependency and needs to be installed explicitly

Quick Start

import useAudio from 'expo-audio-hooks';

function App() {
  const { play, pause, isLoadingAudio } = useAudio(
    { uri: 'https://www.bensound.com/bensound-music/bensound-oblivion.mp3' }
  );

  if (isLoadingAudio) return <Text>Loading...</Text>

  return (
    <View>
      <Text onPress={play}>Play</Text>
      <Text onPress={pause}>Pause</Text>
    </View>
  );
}

Example - A song player

Online Demo

import { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import useAudio from 'expo-audio-hooks';

export default function App() {
  const songList = [
    'https://www.bensound.com/bensound-music/bensound-oblivion.mp3',
    'https://www.bensound.com/bensound-music/bensound-shouldacoulda.mp3',
    'https://www.bensound.com/bensound-music/bensound-supercool.mp3',
  ];
  const [songIndex, setSongIndex] = useState(0);
  const { isLoadingAudio, isPlaying, setIsPlaying } = useAudio({ uri: songList[songIndex] });

  const togglePlay = () => {
    setIsPlaying((value) => !value);
  };

  const goToNextSong = () => {
    setSongIndex((index) => (index + 1) % songList.length);
  };

  if (isLoadingAudio) return <Text>Loading...</Text>

  return (
    <View style={styles.container}>
      <Text>Song #{songIndex}</Text>
      <Text onPress={togglePlay}>{isPlaying ? 'Pause' : 'Play'}</Text>
      <Text onPress={goToNextSong}>Next Song</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'space-evenly',
    flexDirection: 'row',
  },
});

API

The package exports one default export and named exports:

import useAudio from 'expo-audio-hooks'

useAudio(resource, options)

The main React hook to execute an audio.

  • resource - The resource. It can be a URI (stream) or a local audio file.
  • options - An options object.
    • autoPlay ( false ) - If true, the audio will be executed automatically after loading.

Returns

{
    play,
    pause,
    seek,
    unload,
    isPlaying,
    setIsPlaying,
    isLoadingAudio,
    setOnPlaybackStatusUpdate,
    setOnPlaybackTimeUpdate,
}

Why?

It's common to experience crashes and exceptions when working with expo-av. Most of the issues are related with asynchronicity, especially when switching to a new audio source while still loading a previous one.

To properly handle expo-av's asynchronicity, it is necessary to add a quite amount of boilerplate, most of it not obvious unless you have deep understanding of expo-av.

This library simplifies the usage of expo-av.

To-Do

  • Add tests;
  • Typescript;
  • GitHub Actions (testing, auto update dependencies);
  • Document all exported methods;
  • errorAudio object, containing initialization errors (ex. file not found, unsupported format, etc);
  • skip option, to not load the file (like Apollo Hooks);
  • cancel method, to abort current loading;
  • stop method, which is equivalent to pause() + seek(0);
  • Check if we can remove or improve the workarounds (see list below);
  • Extra effects, like volume, fade in, fade out. Ex. pauseWithFade(timeMs);
  • Document how to do standard actions, like to set a loop;

List of Workarounds

The current code works with a few workarounds to deal with I believe are limitations and/or bugs of the expo-av library. It would be great to remove or improve them.

Feel free to work on them and submit PRs, or to suggest ideas on how to improve them.

Workaround 1: Can't cancel an ongoing loading

According to this reply on StackOverflow, there's no way to cancel an already loading audio.

This means if the user start loading a huge mp3 in a slow network and quickly switches to another mp3, the first download will not be cancelled. It will proceed downloading, even though it is not necessary anymore.

We need to cancel loadAsync in situations like: the component is destroyed (unmounted), the source has changed.

To deal with these situations, we implemented the suggested workaround: we wait for the download to finish and then we destroy the object. We detect the download has finished with code on setOnPlaybackStatusUpdate.

Besides the performance issue, this workaround adds a huge chunk of complexity in the useAudio library. We had to implement a queue of pending audio resources, check when the download finishes, etc.

I wonder if there is another way. We have to take a look at the code in expo-av: https://github.com/expo/expo/blob/main/packages/expo-av/src/Audio/Sound.ts

An alternative is to download the file with fetch and pass the downloaded file to loadAsync. Then we can apply AbortController on fetch when needed.

The downside of this alternative is that we lose buffering (do we or can we pass a resource that is still downloading?). And without buffering, it can take a long time with really large files in slow networks, and be problematic in terms of memory and space in low-end devices.

Workaround 2: Unexpected Cannot complete operation because sound is not loaded

Sometimes the audio methods pause/play/stop throw the sound is not loaded exception.

According to the expo-av source code (Audio/Sound.ts#114), this happens because the audio file has not finished loading.

Expo knows when the loading has finished with the private property _loaded.

Although the concept is correct, it seems to have a bug with the current expo-av implementation (Jun 2022, version 11.2.2) that incorrectly sets _loaded as false even if it is loaded.

For example, sometimes this code generates the exception:

const externalFile = { uri: '...' };
const song = new Audio.Sound();
await sound.loadAsync(externalFile);
const status = sound.getStatusAsync();
try {
  sound.playAsync();
} catch (error) {
  console.error(error);
  console.log(status);
}

On the status return, isLoaded is true, but because the exception happens, we know _loaded is false.

So there are two problems:

  • Either loadAsync is returning before it has finished loading or _loaded is not set to true after finishing loading. Either way is wrong;
  • status.isLoading has a different value than _loaded. In my understanding, status.isLoading is the public version of the private _loaded property. If this is correct, than there's another bug here.

I noticed this exception happening in two different occasions:

  • Calling play/pause just after loading the file;
  • Calling play/pause after the song has finished playing, leaving the app aside for a while, and then refreshing the expo code (by saving a source file with the player open);

I thought reporting this bug to the Expo team, but there's a disclaimer saying they will ignore the report unless I can provide a reproductible bug code, which is hard to develop (the bug happens sometimes) and would take a long time to do (we may would have to debug native Android/iOS code).

The way we fixed this bug was to check status.isLoading and also to wrap code on a try/catch. And we also check the isLoadingAudio state that is set after loadAsync.

This triple check (isLoadingAudio, status.isLoading, try/catch) avoids the exception crashing the app, but when it happens, it makes the play/pause commands be ignored. This happens more usually after loading the audio file and playing/pausing right away, but also after saving source-code with the player opened.

An alternative to improve this is to replace the three checks with just the try/catch block alone. The other two are not reliable anyway.

Workaround 3: The bug watchdog

The sound is not loaded bug makes the play/pause commands to be ignored sometimes. As a solution, I implemented a watchdog. It's a setInterval executed every 500 milliseconds or so.

Its code does this:

isPlaying ? song.playAsync() : song.pauseAsync()

This means when a play/pause command is ignored, this watchdog will retry every 500ms.

A better approach for this is to detect the command was ignored (catch block) and set a setTimeout to retry again in 500ms, but only once. If an exception occurs while retrying, it triggers another try in 500ms and so on.

This is less resource-intensive than always be checking every 500ms.

License

MIT