zmxv / react-native-sound

React Native module for playing sound clips
MIT License
2.78k stars 748 forks source link

`mixWithOthers` does not function correctly on iOS. #788

Open dsf3449 opened 2 years ago

dsf3449 commented 2 years ago

:beetle: Description

mixWithOthers permanently stops audio streams on iOS from other apps such as Spotify, even though mixWithOthers is declared as true.

:beetle: What is the observed behavior?

Audio streams are stopped.

:beetle: What is the expected behavior?

The sound being played from another app is played along side the sound from your app.

:beetle: Please post your code:

In a brand new template app:

  1. npx react-native init TestClean
  2. npm i react-native-sounds
  3. pod install
  4. Add loop.mp3 to the xcode project.
  5. Check off background modes: Audio, AirPlay, and Picture in Picture, External accessory communication
  6. In App.js:
    
    import Sound from "react-native-sound";

Sound.setCategory(Playback, true); Sound.setMode(Default);

let sound = new Sound(loop.mp3, Sound.MAIN_BUNDLE, error => { sound.play(); if (error) { console.error(error); } else { console.log('success'); } });


:bulb: **Possible solution**

According to the [developer docs for AVAudioSessionCategoryOptionAllowBluetooth](https://developer.apple.com/documentation/avfaudio/avaudiosessioncategoryoptions/avaudiosessioncategoryoptionallowbluetooth):

`You can set this option only if the audio session category is AVAudioSessionCategoryPlayAndRecord or AVAudioSessionCategoryRecord.`

However, in https://github.com/zmxv/react-native-sound/blob/1aa45f25c8c03ea5c17fac18564c3928bd023113/RNSound/RNSound.m#L175-L180

It is setting this option regardless of the category.

:bulb: **Is there a workaround?**

Removing this declaration `AVAudioSessionCategoryOptionAllowBluetooth` no longer causes the sound to interrupt other apps. I also checked with bluetooth headphones, and there was no difference in behavior with or without this option being present.

Note: I tested this workaround only on `playback` category, iOS 15.4.

Using `patch-package`:

diff --git a/node_modules/react-native-sound/RNSound/RNSound.m b/node_modules/react-native-sound/RNSound/RNSound.m index df3784e..aa97df6 100644 --- a/node_modules/react-native-sound/RNSound/RNSound.m +++ b/node_modules/react-native-sound/RNSound/RNSound.m @@ -175,8 +175,7 @@ - (NSDictionary *)constantsToExport { if (category) { if (mixWithOthers) { [session setCategory:category

:bulb: If the bug is confirmed, would you be willing to create a pull request?

Sure

Is your issue with...

Are you using...

Which versions are you using?

Does the problem occur on...

If your problem is happening on a device, which device?

ucheNkadiCode commented 2 years ago

OH MY GOD thank you so much @dsf3449!!! This is exactly the change I needed to make to get my app sounds to work while spotify is running.

To put the steps succintly.

  1. Set your background audio capability in XCode (or info.plist)

  2. In your instantiation of the Sound module, write

    // Make sure to use Playback and set Mixwithothers to true
    Sound.setCategory('Playback', true)
    // You only need to call the setActive(true) line *once in your code*, and you never set it to false, 
    // EVER unless you no longer want audio to be played in background. 
    // You do not need to try calling setActive(false) at any point in the onCompletion methods
    Sound.setActive(true)
  3. Then navigate to node_modules/react-native-sound/RNSound/RNSound.m

  4. Edit the line below as such Change

    if (category) {
        if (mixWithOthers) {
            [session setCategory:category
                     withOptions:AVAudioSessionCategoryOptionMixWithOthers | 
                           AVAudioSessionCategoryOptionAllowBluetooth
                           error:nil];
        } else {
            [session setCategory:category error:nil];
        }
    }

TO

if (category) {
        if (mixWithOthers) {
            [session setCategory:category
                     withOptions:AVAudioSessionCategoryOptionMixWithOthers 
                    //  | AVAudioSessionCategoryOptionAllowBluetooth // I moved the '|' to the next line and commented it out
                           error:nil];
        } else {
            [session setCategory:category error:nil];
        }
    }
pedpess commented 2 years ago

Thanks, guys! I had reported the same here as well, but as a feature request as I didn't know the lib supported this behavior.

https://github.com/zmxv/react-native-sound/issues/781

pedpess commented 2 years ago

I have a peculiar case where I want the sound to play/mix together with Spotify with the device looking and the app in the background. Did you @ucheNkadiCode and @dsf3449 manage to make it work? I have tried iOS 15.4 it didn't

swey commented 1 year ago

We had the same issue. We have sounds playing with Sound.setCategory('Ambient', true); which worked fine in the past and stopped working in the meantime.

I can confirm that the patch of @dsf3449 works and solves the issue. Thank you!

@ucheNkadiCode You should check out https://www.npmjs.com/package/patch-package instead of manually editing the file after each npm/yarn install.

pierroo commented 11 months ago

More than a year later; do we still have to use this patch? I am still facing it with latest version :/

dsf3449 commented 11 months ago

@pierroo I don't believe the repo author has made any changes regarding this specific issue. Many PRs outstanding and no releases for over 1.5 years. I would assume it to be abandoned at this time.

As for alternatives, unfortunately the state of audio packages for react native seems to be quite fragmented. This reddit thread lists at least 6 different options depending on your use case.

Koxx3 commented 10 months ago

hello !

I modified the RNSound.m ... but the music still stop when my app starts. Here is my code :

  init = async () => {

        Sound.setCategory('Playback', true);
        Sound.setActive(true);

    }

    play = async () => {

        try {

                var ding = new Sound("test.mp3", Sound.MAIN_BUNDLE, (error) => {
                    if (error) {
                        console.log('Sound / play / failed to load the sound', error);
                    }
                    else {

                        ding.play(async (success) => {
                            if (success) {
                                ding.release()
                            }
                        });
                    }
                });
            }
            console.log('Sound / play / exit');
        } catch (error) {
            console.log('Sound / play / error', error);
        }
    }
RCT_EXPORT_METHOD(setCategory
                  : (NSString *)categoryName mixWithOthers
                  : (BOOL)mixWithOthers) {
    AVAudioSession *session = [AVAudioSession sharedInstance];
    NSString *category = nil;

    if ([categoryName isEqual:@"Ambient"]) {
        category = AVAudioSessionCategoryAmbient;
    } else if ([categoryName isEqual:@"SoloAmbient"]) {
        category = AVAudioSessionCategorySoloAmbient;
    } else if ([categoryName isEqual:@"Playback"]) {
        category = AVAudioSessionCategoryPlayback;
    } else if ([categoryName isEqual:@"Record"]) {
        category = AVAudioSessionCategoryRecord;
    } else if ([categoryName isEqual:@"PlayAndRecord"]) {
        category = AVAudioSessionCategoryPlayAndRecord;
    }
#if TARGET_OS_IOS
    else if ([categoryName isEqual:@"AudioProcessing"]) {
        category = AVAudioSessionCategoryAudioProcessing;
    }
#endif
    else if ([categoryName isEqual:@"MultiRoute"]) {
        category = AVAudioSessionCategoryMultiRoute;
    }

    if (category) {
        if (mixWithOthers) {
            [session setCategory:category
                     withOptions:AVAudioSessionCategoryOptionMixWithOthers
                     // | AVAudioSessionCategoryOptionAllowBluetooth
                           error:nil];
        } else {
            [session setCategory:category error:nil];
        }
    }
}