expo: Sound and Android ExoPlayer bugs

Environment

Environment: OS: macOS High Sierra 10.13.4 Node: 10.4.0 Yarn: 1.7.0 npm: 6.1.0 Watchman: 4.7.0 Xcode: Xcode 9.4 Build version 9F1027a Android Studio: 3.1 AI-173.4819257

Packages: (wanted => installed) expo: ^27.0.0 => 27.0.2 react: 16.3.1 => 16.3.1 react-native: https://github.com/expo/react-native/archive/sdk-27.0.0.tar.gz => 0.55.2

Diagnostics report: https://exp-xde-diagnostics.s3.amazonaws.com/devops-e4c41f10-e70a-4661-b6d2-78584c7ccd9f.tar.gz

Steps to Reproduce

  1. Create a simple audio application
  2. Load to an internal storage a lot of short mp3 like simple English words
  3. Use the Expo application on your Android device to play/unload audio, quickly and randomly
  4. Look at the console or debugger and logcat

Expected Behavior

All words must be loaded and played without problems, the behavior of the audio player must be predictable. If it’s needed I want to receive raw information about counts of loaded files and available resources to play sounds without any problems. Maybe I need to control this process in my application to know how I can dispose of resources on a system level.

(Write what you thought would happen.)

Actual Behavior

After some time, js and native part have been broken. I use expo.FileSystem to cache sounds so that all files are loaded in advance.

Logcat gists

https://gist.github.com/MikePodgorniy/c20e1c5009cf92612500d19f9264a47d https://gist.github.com/MikePodgorniy/c1ae5a06db9819753c11239511168e55

Before play sound I check it:

{
10:17:58 [exp]   "exists": true,
10:17:58 [exp]   "isDirectory": false,
10:17:58 [exp]   "modificationTime": 1528103008,
10:17:58 [exp]   "size": 8195,
10:17:58 [exp]   "uri": "file:///data/user/0/host.exp.exponent/files/ExperienceData/%2540devops%252FSmartWords-x/soundCache/HyLwxFzeQ.mp3",
10:17:58 [exp] }

After that i load sound await this.soundObject.loadAsync({ uri }, { volume: this.volume }, true);

playbackStatus Object {
10:17:59 [exp]   "androidImplementation": "SimpleExoPlayer",
10:17:59 [exp]   "didJustFinish": false,
10:17:59 [exp]   "durationMillis": 1358,
10:17:59 [exp]   "isBuffering": false,
10:17:59 [exp]   "isLoaded": true,
10:17:59 [exp]   "isLooping": false,
10:17:59 [exp]   "isMuted": false,
10:17:59 [exp]   "isPlaying": false,
10:17:59 [exp]   "playableDurationMillis": 1358,
10:17:59 [exp]   "positionMillis": 0,
10:17:59 [exp]   "progressUpdateIntervalMillis": 500,
10:17:59 [exp]   "rate": 1,
10:17:59 [exp]   "shouldCorrectPitch": false,
10:17:59 [exp]   "shouldPlay": false,
10:17:59 [exp]   "uri": "/data/user/0/host.exp.exponent/files/ExperienceData/%40devops%2FSmartWords-x/soundCache/HyLwxFzeQ.mp3",
10:17:59 [exp]   "volume": 1,
10:17:59 [exp] }

Than I check sound status and play it

play = async () => {
    try {
      console.log('<< play >> this.isLoaded:', this.isLoaded);
      const status = await this.soundObject.getStatusAsync();
      console.log(status);

      if (this.isLoaded) {
        this.sendStatus(AUDIO_PLAYING);
        this.isPlaying = true;
        await this.soundObject.playAsync();
        console.log('done');
      }
      // this.isPlaying = false;
    } catch (error) {
      console.log('>>>>>>>> ALARM PLAY', error);
    }
  };

Result:

10:17:59 [exp] << play >> this.isLoaded: true
10:17:59 [exp] Object {
10:17:59 [exp]   "androidImplementation": "SimpleExoPlayer",
10:17:59 [exp]   "didJustFinish": false,
10:17:59 [exp]   "durationMillis": 1358,
10:17:59 [exp]   "isBuffering": false,
10:17:59 [exp]   "isLoaded": true,
10:17:59 [exp]   "isLooping": false,
10:17:59 [exp]   "isMuted": false,
10:17:59 [exp]   "isPlaying": false,
10:17:59 [exp]   "playableDurationMillis": 1358,
10:17:59 [exp]   "positionMillis": 0,
10:17:59 [exp]   "progressUpdateIntervalMillis": 500,
10:17:59 [exp]   "rate": 1,
10:17:59 [exp]   "shouldCorrectPitch": false,
10:17:59 [exp]   "shouldPlay": false,
10:17:59 [exp]   "uri": "/data/user/0/host.exp.exponent/files/ExperienceData/%40devops%2FSmartWords-x/soundCache/HyLwxFzeQ.mp3",
10:17:59 [exp]   "volume": 1,
10:17:59 [exp] }
10:17:59 [exp] >>>>>>>> ALARM PLAY [Error: Cannot complete operation because sound is not loaded.]

also I have next behavior of expo app in setOnPlaybackStatusUpdate callback:

 this.soundObject.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
onPlaybackStatusUpdate = playbackStatus => {
    console.log('playbackStatus', playbackStatus);
    if (!playbackStatus.isLoaded) {
      // Update your UI for the unloaded state
      if (playbackStatus.error) {
        console.log(`Encountered a fatal error during playback: ${playbackStatus.error}`);
        // Send Expo team the error on Slack or the forums so we can help you debug!
      }
    } 
 /* some code here */
}
playbackStatus Object {
11:22:34 [exp]   "androidImplementation": "SimpleExoPlayer",
11:22:34 [exp]   "didJustFinish": false,
11:22:34 [exp]   "durationMillis": 1358,
11:22:34 [exp]   "isBuffering": false,
11:22:34 [exp]   "isLoaded": true,
11:22:34 [exp]   "isLooping": false,
11:22:34 [exp]   "isMuted": false,
11:22:34 [exp]   "isPlaying": false,
11:22:34 [exp]   "playableDurationMillis": 1358,
11:22:34 [exp]   "positionMillis": 0,
11:22:34 [exp]   "progressUpdateIntervalMillis": 500,
11:22:34 [exp]   "rate": 1,
11:22:34 [exp]   "shouldCorrectPitch": false,
11:22:34 [exp]   "shouldPlay": false,
11:22:34 [exp]   "uri": "/data/user/0/host.exp.exponent/files/ExperienceData/%40devops%2FSmartWords-x/soundCache/HyLwxFzeQ.mp3",
11:22:34 [exp]   "volume": 1,
11:22:34 [exp] }
11:22:34 [exp] load, this.isKilled: false
11:22:34 [exp] << play >> this.isLoaded: true
11:22:35 [exp] Object {
11:22:35 [exp]   "androidImplementation": "SimpleExoPlayer",
11:22:35 [exp]   "didJustFinish": false,
11:22:35 [exp]   "durationMillis": 1358,
11:22:35 [exp]   "isBuffering": false,
11:22:35 [exp]   "isLoaded": true,
11:22:35 [exp]   "isLooping": false,
11:22:35 [exp]   "isMuted": false,
11:22:35 [exp]   "isPlaying": false,
11:22:35 [exp]   "playableDurationMillis": 1358,
11:22:35 [exp]   "positionMillis": 0,
11:22:35 [exp]   "progressUpdateIntervalMillis": 500,
11:22:35 [exp]   "rate": 1,
11:22:35 [exp]   "shouldCorrectPitch": false,
11:22:35 [exp]   "shouldPlay": false,
11:22:35 [exp]   "uri": "/data/user/0/host.exp.exponent/files/ExperienceData/%40devops%2FSmartWords-x/soundCache/HyLwxFzeQ.mp3",
11:22:35 [exp]   "volume": 1,
11:22:35 [exp] }
11:22:35 [exp] playbackStatus Object {
11:22:35 [exp]   "androidImplementation": "SimpleExoPlayer",
11:22:35 [exp]   "didJustFinish": false,
11:22:35 [exp]   "durationMillis": 1358,
11:22:35 [exp]   "isBuffering": false,
11:22:35 [exp]   "isLoaded": true,
11:22:35 [exp]   "isLooping": false,
11:22:35 [exp]   "isMuted": false,
11:22:35 [exp]   "isPlaying": true,
11:22:35 [exp]   "playableDurationMillis": 1358,
11:22:35 [exp]   "positionMillis": 0,
11:22:35 [exp]   "progressUpdateIntervalMillis": 500,
11:22:35 [exp]   "rate": 1,
11:22:35 [exp]   "shouldCorrectPitch": false,
11:22:35 [exp]   "shouldPlay": true,
11:22:35 [exp]   "uri": "/data/user/0/host.exp.exponent/files/ExperienceData/%40devops%2FSmartWords-x/soundCache/HyLwxFzeQ.mp3",
11:22:35 [exp]   "volume": 1,
11:22:35 [exp] }
11:22:35 [exp] done
11:22:35 [exp] playbackStatus Object {
11:22:35 [exp]   "error": "Player error: null",
11:22:35 [exp]   "isLoaded": false,
11:22:35 [exp] }
11:22:35 [exp] Encountered a fatal error during playback: Player error: null

and Promise Rejection somewhere in expo.Sound after next try to load sound or play it

11:35:56 [exp] [Unhandled promise rejection: Error: Player does not exist.]
- node_modules/react-native/Libraries/BatchedBridge/NativeModules.js:121:32 in createErrorFromErrorData
- node_modules/react-native/Libraries/BatchedBridge/NativeModules.js:78:32 in <unknown>
- node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:398:4 in MessageQueue.__invokeCallback
- node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:137:11 in <unknown>
- node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:314:6 in MessageQueue.__guardSafe
- node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:136:9 in MessageQueue.invokeCallbackAndReturnFlushedQueue
- node_modules/metro/src/lib/polyfills/require.js:173:4 in <unknown>
- node_modules/metro/src/lib/polyfills/require.js:85:5 in process.<anonymous>
* events.js:182:13 in process.emit
* internal/child_process.js:811:12 in emit

Error: Cannot complete operation because sound is not loaded.
    at Sound._performOperationAndHandleStatusAsync$ (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:108647:23)
    at tryCatch (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:17044:19)
    at Generator.invoke [as _invoke] (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:17217:24)
    at Generator.prototype.(anonymous function) [as next] (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:17087:23)
    at tryCatch (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:17044:19)
    at invoke (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:17120:22)
    at /Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:17148:13
    at tryCallTwo (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:16382:7)
    at doResolve (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:16546:15)
    at new Promise (/Users/mike/Documents/Work/EnglishDom/EDApp/.vscode/.react/main.bundle:16405:5)

Reproducible Demo

If you want I can send you access to expo app and instruction how to use the app (it have Russian localization)

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 3
  • Comments: 40 (5 by maintainers)

Commits related to this issue

Most upvoted comments

Hi guys, I’m working on a game that uses many short mp3 audios, and I found a solution for the Audio issues on android (compiled and in expo browser) that causes the player to broke from time to time leaving the app without audio. The solution is just to unload audios once they’ve finished playing and don’t reuse the audio objects to play many times the same audio. That way, I stopped having issues on android.

I know is not the best solution but it works fine for me and doesn’t looks like I’m loosing performance for not reusing the Sound object 😄

import { Audio } from 'expo';

function playSound(name, sound){
	console.log('Playing '+name);
	Audio.Sound.createAsync(
		sound,
		{ shouldPlay: true }
	).then((res)=>{
		res.sound.setOnPlaybackStatusUpdate((status)=>{
			if(!status.didJustFinish) return;
			console.log('Unloading '+name);
			res.sound.unloadAsync().catch(()=>{});
		});
	}).catch((error)=>{});
}

const clickSound = require('../assets/sounds/click.mp3');
playSound('click', clickSound);

// To test if it stops working at some point:
// setInterval(()=>{playSound('click',clickSound);},200);

@brentvatne, @sjchmiela, please reopen issue

Are there any updates on this?

@kashifsulaiman – use webview + html5 audio on android I am not kidding 😄

I eventually managed to work around most of the issues by wrapping expo-av and always making sure to unload/load before playing/looping/stopping. It also makes it fairly easy to switch to a different library if needed.

It can be simplified a lot if you remove the support for fading in/out, looping and continueFromPreviousPosition. It’s just stuff that I needed badly for my use-case. Feel free to get inspiration from it 😃

import { Audio } from 'expo-av'
import logger from '../helpers/logger'
import { audio as audioFiles } from '../../data/media'

class Track {
  constructor(id) {
    this._id = id
    this._sound = new Audio.Sound()
    this._fadeTimeout = null
    this._loopHandlerWorking

    this.status = {
      loaded: false,
      pauseTime: 0,
      volume: 1
    }
  }

  load = () => new Promise (async (resolve, reject) => {
    const { _sound, _id, status } = this

    try {
      await _sound.loadAsync(audioFiles[_id])
      status.loaded = true
      resolve()
    } catch (error) {
      reject(error)
    }
  })

  unload = () => new Promise (async (resolve, reject) => {
    const { _sound, status } = this

    try {
      status.loaded = false
      await _sound.unloadAsync()
      resolve()
    } catch (error) {
      reject(error)
    }
  })

  setVolume = (volume) => new Promise (async (resolve, reject) => {
    const { _sound, status } = this

    if (!status.loaded) return resolve()

    try {
      await _sound.setVolumeAsync(volume)
      status.volume = volume
      resolve()
    } catch (error) {
      reject(error)
    }
  })

  setCurrentTime = (time) => new Promise (async (resolve, reject) => {
    const { _sound, status } = this

    if (!status.loaded) return resolve()

    try {
      await _sound.setStatusAsync({ positionMillis: time })
      resolve()
    } catch (error) {
      reject(error)
    }
  })

  play = ({
    loop = true,
    continueFromPreviousPosition = true,
    volume = 1
  }) => new Promise (async (resolve, reject) => {
    const { _sound, setCurrentTime, setVolume, fade, setTrackToLooping, load, status } = this

    try {
      if (!status.loaded) {
        await load()
      }

      const shouldFadeIn = continueFromPreviousPosition && status.pauseTime !== 0 ? true : false
      await setCurrentTime(continueFromPreviousPosition ? status.pauseTime : 0)
      await setVolume(shouldFadeIn ? 0 : volume)

      await _sound.playAsync()

      if (shouldFadeIn) {
        await fade(volume)
      }

      if (loop) {
        await setTrackToLooping()
      }

      resolve()
    } catch (error) {
      reject(error)
    }
  })

  pause = () => new Promise(async (resolve, reject) => {
    const { _sound, status, fade, unload } = this

    if (!status.loaded) return resolve()

    try {
      const { positionMillis } = await _sound.getStatusAsync()
      status.pauseTime = positionMillis ? positionMillis : 0
      await fade(0)
      await unload()
      resolve()
    } catch (error) {
      reject(error)
    }
  })

  stop = () => new Promise(async (resolve, reject) => {
    const { fade, unload, status, _sound } = this

    if (!status.loaded) return resolve()

    try {
      const { isPlaying } = await _sound.getStatusAsync()

      if (isPlaying) {
        await fade(0)
      }

      await unload()
      resolve()
    } catch (error) {
      reject(error)
    }
  })

  fade = (toVolume) => new Promise((resolve, reject) => {
    const { status, _fadeTimeout, setVolume } = this

    if (status.volume === toVolume) return

    if (_fadeTimeout) {
      clearTimeout(_fadeTimeout)
    }

    const start = Math.floor(status.volume * 10)
    const end = toVolume * 10
    let currVolume = start

    const loop = async () => {
      if (currVolume !== end) {
        start < end ? currVolume++ : currVolume--
        await setVolume(currVolume / 10)
        this._fadeTimeout = setTimeout(loop, 150)
      } else {
        clearTimeout(_fadeTimeout)
        this._fadeTimeout = null
        resolve()
      }
    }

    this._fadeTimeout = setTimeout(loop, 5)
  })

  setTrackToLooping = () => new Promise(async (resolve, reject) => {
    try {
      this._loopHandlerWorking = false
      await this._sound.setOnPlaybackStatusUpdate(this._loopHandler)
      resolve()
    } catch (error) {
      reject(error)
    }
  })

  _loopHandler = async (status) => {
    const { isLoaded, isPlaying, durationMillis, positionMillis, volume } = status

    if (
      !this._loopHandlerWorking &&
      isPlaying &&
      isLoaded &&
      (durationMillis - positionMillis) < 1500
    ) {
      try {
        this._loopHandlerWorking = true
        await this.stop()
        await this.play({ volume, loop: true })
      } catch (error) {
        logger.error(error)
      } finally {
        this._loopHandlerWorking = false
      }
    }
  }
}

export default Track

I’ve submitted an upstream PR with which I’ve managed to successfully tap <kbd>Stress test</kbd> and <kbd>Play random sound</kbd> consecutively a couple of times and the player worked as expected. I think the PR should get released with SDK 29. 🙂 I hope it fixes your issues and if not I’ll get back to debugging. 💪

@sjchmiela bad news…

exo still drop audio playback 😦

[17:01:00] playbackStatus Object {
[17:01:00]   "error": "Player error: AudioTrack init failed: 0, Config(22050, 4, 14176)",
[17:01:00]   "isLoaded": false,
[17:01:00] }
[17:01:00] Encountered a fatal error during playback: Player error: AudioTrack init failed: 0, Config(22050, 4, 14176)

Sometimes the player working well, but sometimes this same error goes on! Please reopen issue, I’m using SDK 31. Mostly occurring when refreshing during development.

Every time when you press a button, the app will create a new expo.Audio.Sound instance, load a sound file and play it https://github.com/MikePodgorniy/AndroidSound/blob/872f458aa1daa0f849bac7b729c23b239e0ce880/src/Sound.js#L97

How should I reload the sound? Expo-Client can’t play any sound after those errors, I need to restrart expo (or my app) and after that I can play sounds.

I need bulletproof solution for this case 🔫 How to fix it by design?

Hi there. I work as a QA engineer on that project. And I’m just exhausted from getting negative reports from our users. I hope you fix it soon.

Hi guys! I’ve recently been on vacation and after that we’ve been busy preparing SDK 29, so I couldn’t take my time to fix those problems. Fortunately, I have an idea that could help mitigate those problems at least a little bit which I’ll try to test as soon as possible. 🙂

But I think the tasks that you set can possibly be solved, caching with access from webview and possibly the use of high-level libraries like Howler.js I have issues with audio on my application. I solved it:

  • using WebAudio in invisible WebView
  • use Howler as audio engine
  • use postMessage or injectJavaScript for playing sound Main issues is a latency between clicking and sound playing.

@MikePodgorniy thanks for the workaround! encountered the exact same problem

@gkufera @terribleben Guys, do you have any recommendations, how can I fix this now in js-code, I’ll be glad to hear your point of view on this error. This is a production application with 10k + users, and every day I receive comments from users and my boss about it 🤒🌵

Yes, sometimes it works and sometimes it doesn’t work. iOS doesn’t have any kind of issue. Is there any work around for this audio problem? Not sure how the Expo API Audio player example works fine everytime I listen to that?

I believe that I’m facing the same issue. After about 7 minutes playing, the player stopped and it fires “[Unhandled promise rejection: Error: Player does not exist.]” when I tried to press Play button again.

@sjchmiela bad news… 🌧

exo still drop audio playback 😦

[17:01:00] playbackStatus Object {
[17:01:00]   "error": "Player error: AudioTrack init failed: 0, Config(22050, 4, 14176)",
[17:01:00]   "isLoaded": false,
[17:01:00] }
[17:01:00] Encountered a fatal error during playback: Player error: AudioTrack init failed: 0, Config(22050, 4, 14176)