expo: expo-av Video crash in production: EXAVPlayerData observeValueForKeyPath:ofObject:change:context:

🐛 Bug Report

Hi there,

I’m using “expo-av”: “~8.1.0” in a bare workflow within my App joinweburn.com on iOS. My users can do short workouts where 4-12 different videos are shown in a row. Also, there is a countdown (via Sound) running every 30 seconds.

Yesterday, I released a new version and am now getting some crashes in production. During testing, we didn’t experience crashes. Luckily, I’ve set up firbase crashlytics & analytics.

I debugged with XCode and saw that my App uses up to 300 mb during playing videos. I also saw a memory leak which lead to higher memory allocation after every workout. I fixed this now and will ship it to Apple today. The App now stays below 300 mb.

I really think this might be an issue with the user available memory (RAM). All affected users only had free memory below 300 mb - sometimes even only 30 mb. But let’s find this out. Here’s the stacktrace:

Crashed: com.apple.main-thread
0  libsystem_kernel.dylib         0x1b2205dd0 __abort_with_payload + 8
1  libsystem_kernel.dylib         0x1b21ff924 abort_with_payload_wrapper_internal + 100
2  libsystem_kernel.dylib         0x1b21ff8c0 abort_with_payload_wrapper_internal + 26
3  libobjc.A.dylib                0x1b1843478 _objc_fatalv(unsigned long long, unsigned long long, char const*, char*) + 112
4  libobjc.A.dylib                0x1b18433d0 __objc_error + 38
5  libobjc.A.dylib                0x1b185b2c4 weak_entry_insert(weak_table_t*, weak_entry_t*) + 314
6  libobjc.A.dylib                0x1b185bea4 objc_storeWeak + 360
7  weburn                         0x1008ab880 -[EXAVPlayerData observeValueForKeyPath:ofObject:change:context:] + 680 (EXAVPlayerData.m:680)
8  Foundation                     0x1b30b74b8 NSKeyValueNotifyObserver + 288
9  Foundation                     0x1b30b984c NSKeyValueDidChange + 364
10 Foundation                     0x1b30b6d6c NSKeyValueDidChangeWithPerThreadPendingNotifications.llvm.18145020194561184386 + 140
11 AVFoundation                   0x1b862df4c __avplayeritem_fpItemNotificationCallback_block_invoke + 2216
12 libdispatch.dylib              0x1b20a8a38 _dispatch_call_block_and_release + 24
13 libdispatch.dylib              0x1b20a97d4 _dispatch_client_callout + 16
14 libdispatch.dylib              0x1b2057008 _dispatch_main_queue_callback_4CF$VARIANT$mp + 1068
15 CoreFoundation                 0x1b25fcb20 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
16 CoreFoundation                 0x1b25f7a58 __CFRunLoopRun + 1924
17 CoreFoundation                 0x1b25f6fb4 CFRunLoopRunSpecific + 436
18 GraphicsServices               0x1b47f879c GSEventRunModal + 104
19 UIKitCore                      0x1dee58c38 UIApplicationMain + 212
20 weburn                         0x10082a80c main + 14 (main.m:14)
21 libdyld.dylib                  0x1b20ba8e0 start + 4

Line 7 is what make me think this is an expo-av related issue:

0x1008ab880 -[EXAVPlayerData observeValueForKeyPath:ofObject:change:context:] + 680 (EXAVPlayerData.m:680)

Do you have any ideas what this exactly means and how to avoid crashes like this?

Environment

App Target: iOS & Expo Bare Workflow

Expo CLI 3.13.1 environment info: System: OS: macOS 10.15.3 Shell: 5.7.1 - /bin/zsh Binaries: Node: 12.3.1 - /usr/local/bin/node Yarn: 1.16.0 - /usr/local/bin/yarn npm: 6.9.0 - /usr/local/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman IDEs: Android Studio: 3.5 AI-191.8026.42.35.5977832 Xcode: 11.4/11E146 - /usr/bin/xcodebuild npmPackages: expo: ^37.0.0 => 37.0.5 react: ^16.13.1 => 16.13.1 react-native: 0.61.4 => 0.61.4 npmGlobalPackages: expo-cli: 3.13.1

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 4
  • Comments: 18 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Hi! We’ve fixed various stability issues with expo-av and we think we fixed this issue as well. I’ve released a test-version for you to try out and I’d love to hear whether this solves the problem for you.

You can install it in a bare project using yarn install expo-av@8.4.0. You can find the changelog here: https://github.com/expo/expo/blob/master/packages/expo-av/CHANGELOG.md

Let us know whether this fixes the problem for you!

hey @IjzerenHein

Here is the actual code used in production that loads / unloads the sounds (we currently experience this for sound, not video, but they both use the underlying expo-av)

//
// inspired from react-timer-hook
// https://github.com/amrlabib/react-timer-hook/blob/master/src/useTimer.js
//
import {useEffect, useRef} from 'react';
import {Audio} from 'expo-av';
import {AVPlaybackStatus} from 'expo-av/build/AV';

/**
 *
 * Hook reponsible of loading / unloading the sounds needed by a component
 * @param props
 */
export default function useSounds(
  sources: Array<number>,
  playFirstOne: boolean,
) {
  const soundsRef = useRef<Audio.Sound[]>();

  useEffect(() => {
    //promise to load all sources, all at once
    Promise.all(
      sources.map((source, index) =>
        Audio.Sound.createAsync(
          source,
          {
            shouldPlay: playFirstOne && index === 0,
          },
          handlePlaybackStatusUpdate(index),
        ),
      ),
    )
      .then(resources => {
        //TODO: there is a possible race condition, namely the sources change BEFORE all promises get resolved
        //before assigning the sounds to the ref, we should determine if that scenario happened
        //and instead unloadAsync the sounds to avoid memory leaks;

        soundsRef.current = resources.map(resource => resource.sound);
      })
      .catch(() => {
        //add the moment, if the loading fails, simply avoid playing any sounds for this component
      });

    //setup the mode for the audio session
    Audio.setAudioModeAsync({
      playsInSilentModeIOS: true,
      allowsRecordingIOS: false,
      staysActiveInBackground: true,
      interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DUCK_OTHERS,
      shouldDuckAndroid: true,
      interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DUCK_OTHERS,
      playThroughEarpieceAndroid: true,
    });

    return () => {
      if (soundsRef.current) {
        //release the memory
        soundsRef.current.map(sound => sound.unloadAsync());
        soundsRef.current = undefined;
      }
    };
  }, sources); //eslint-disable-line react-hooks/exhaustive-deps

  //
  // Fix a bug where the spotify sound level would not raise back when the sound just finished
  //
  const handlePlaybackStatusUpdate = (index: number) => (
    playbackStatus: AVPlaybackStatus,
  ) => {
    if (
      playbackStatus.isLoaded &&
      playbackStatus.didJustFinish &&
      soundsRef.current
    ) {
      soundsRef?.current[index].stopAsync();
    }
  };

  return soundsRef;
}

And then, where we need it use something like

useSounds([require('assets/sounds/done.aac')], true);

UPDATE: No looping used

UPDATE2: Here is another, more complex usage of the hook


  const playFirstOne = props.countdown === marks[0]; //whether or not to play the first one imediatelly after loading
  const soundsRef = useSounds(sounds, playFirstOne);
  const [currentMark, setCurrentMark] = useState(playFirstOne ? 0 : -1); //this accounts for the first mount (which has no sound available)

  //use an effect to play only 1 sound, the correct one (and to stop when the countdown is unmounted)
  useEffect(
    () => {
      if (soundsRef.current && currentMark < soundsRef.current.length) {
        soundsRef.current[currentMark].playAsync();
      }

      //increment the current mark, so the next time the effect is run, the proper sound is played
      setCurrentMark(currentMark + 1);
    },

    //create a boolean dep list based on the position of secondsLeft relative to the marks
    //[false, false] vs [true, false] vs [true, true] etc.
    marks.map(mark => secondsLeft < mark), //eslint-disable-line react-hooks/exhaustive-deps
  );


@andreibarabas I’ve only done a brief review of the code so far, but it looks like not all observers are removed before the PlayerData object is released.

@IjzerenHein - one last thing, noticed the crash happening when in Battery -> Low Power Mode is activated, which is likely to happen if you < 20% of battery

This is useful, thanks for letting me know @andreibarabas 👍