Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] How to set output audio device? #39

Open
mirkancal opened this issue Sep 9, 2021 · 23 comments
Open

[Question] How to set output audio device? #39

mirkancal opened this issue Sep 9, 2021 · 23 comments

Comments

@mirkancal
Copy link

Thanks to this package, we can get audio devices as a Future or Stream. I'm trying to cast audio to one of those devices. I couldn't figure out how I can do it. Is it possible with this package?

@ryanheise
Copy link
Owner

This plugin exposes the AVAudioSession API on iOS and the AudioManager API on Android (via the class AndroidAudioManager), which allow you to route audio. Since it's platform specific, I would recommend either looking up the platform docs for AVAudioSession/AudioManager (or you'll probably find answers on StackOverflow for how to use these two APIs to achieve the desired effect.)

@mirkancal
Copy link
Author

I check couple SO posts, I read that it's not possible on iOS but it was an old answer. Anyway, I solve the iOS part by simple opening AirPlay. With this package flutter_to_airplay. Right now I'm looking to trigger to open this media view(native device picker view) on Android within the app. Since audio_service package register the audio within the OS, once I can trigger this native device picker, I'll be more than enough.

Back to the topic, for programmatically changing the audio route, I've checked this SO post. I was looking for simple API call, like audioManager.changeOutputDevice(deviceId: device.id) but the example codes I see on the SO is more like stopping something first and then setting another thing, and I don't see anything related to the devices but calls like setSpeakerphoneOn or setBluetoothScoOn. I also see just_audio package has this casting feature on the roadmap, so if you can point me to where it's been developing at the moment, I'd love contribute.

@ryanheise
Copy link
Owner

Casting is another matter altogether, but you can take a look at audio_cast for the Android side of things.

As for the traditional audio devices, there are various answers on S/O:

@Vilmir
Copy link

Vilmir commented Jan 7, 2022

I have not found any working option to list and change the audio output routes on iOS, apparently Apple does not want this to be modified programmatically.
I have finally decided to opt in for invoking the OS audio route picker from my app. On iOS it's AVRoutePickerView, on Android MediaRouteSelector.
You can display native views in Flutter using the official guide for native views.
It works pretty well after a minimal amount of work on ios, on Android the choice of the rendering will have an impact on performances on Android 9 and earlier versions. It may involve more testing...

I wonder how Spotify can display their own customized view to pick the audio output.

@ryanheise
Copy link
Owner

Some more info on iOS:

https://developer.apple.com/forums/thread/62954

It looks like you can programmatically set a Bluetooth input device, but you can't programmatically set a Bluetooth output device (unless that device is ALSO an input device, e.g. headset with a mic). For an output-only device, it needs to be done through a UI widget, so someone else would need to create a package for that to handle that use case.

@ryanheise
Copy link
Owner

On Android, the typically recommended solution involves deprecated methods. I think I may need to eventually implement the newer API (probably using the new JNIgen):

https://developer.android.com/reference/androidx/mediarouter/media/package-summary

@ryanheise
Copy link
Owner

Also suggestions from @Peng-Qian in #95 which I have folded into this issue. On the Android side, it seems to align with the usual S/O advice (and hence should probably be done with the newer MediaRouter API). Note that until these convenience methods are implemented, you can still achieve the goal with the current release of audio_session by manually invoking the same lower level methods as described in the solutions linked above (e.g. #95 ).

@ryanheise
Copy link
Owner

Here is an implementation of the suggested solutions (completely untested):

  final _androidAudioManager =
      !kIsWeb && Platform.isAndroid ? AndroidAudioManager() : null;
  final _avAudioSession = !kIsWeb && Platform.isIOS ? AVAudioSession() : null;

  Future<bool> switchToSpeaker() async {
    if (_androidAudioManager != null) {
      await _androidAudioManager!.setMode(AndroidAudioHardwareMode.normal);
      await _androidAudioManager!.stopBluetoothSco();
      await _androidAudioManager!.setBluetoothScoOn(false);
      await _androidAudioManager!.setSpeakerphoneOn(true);
    } else if (_avAudioSession != null) {
      await _avAudioSession!
          .overrideOutputAudioPort(AVAudioSessionPortOverride.speaker);
    }
    return true;
  }

  Future<bool> switchToReceiver() async {
    if (_androidAudioManager != null) {
      _androidAudioManager!.setMode(AndroidAudioHardwareMode.inCommunication);
      _androidAudioManager!.stopBluetoothSco();
      _androidAudioManager!.setBluetoothScoOn(false);
      _androidAudioManager!.setSpeakerphoneOn(false);
      return true;
    } else if (_avAudioSession != null) {
      return await _switchToAnyIosPortIn({AVAudioSessionPort.builtInMic});
    }
    return false;
  }

  Future<bool> switchToHeadphones() async {
    if (_androidAudioManager != null) {
      _androidAudioManager!.setMode(AndroidAudioHardwareMode.inCommunication);
      _androidAudioManager!.stopBluetoothSco();
      _androidAudioManager!.setBluetoothScoOn(false);
      _androidAudioManager!.setSpeakerphoneOn(false);
      return true;
    } else if (_avAudioSession != null) {
      return await _switchToAnyIosPortIn({AVAudioSessionPort.headsetMic});
    }
    return true;
  }

  Future<bool> switchToBluetooth() async {
    if (_androidAudioManager != null) {
      await _androidAudioManager!
          .setMode(AndroidAudioHardwareMode.inCommunication);
      await _androidAudioManager!.startBluetoothSco();
      await _androidAudioManager!.setBluetoothScoOn(true);
      return true;
    } else if (_avAudioSession != null) {
      return await _switchToAnyIosPortIn({
        AVAudioSessionPort.bluetoothLe,
        AVAudioSessionPort.bluetoothHfp,
        AVAudioSessionPort.bluetoothA2dp,
      });
    }
    return false;
  }

  Future<bool> _switchToAnyIosPortIn(Set<AVAudioSessionPort> ports) async {
    if ((await _avAudioSession!.currentRoute)
        .outputs
        .any((r) => ports.contains(r.portType))) {
      return true;
    }
    for (var input in await _avAudioSession!.availableInputs) {
      if (ports.contains(input.portType)) {
        await _avAudioSession!.setPreferredInput(input);
      }
    }
    return false;
  }

On Android, I did not provide the setWiredHeadsetOn because it is not only deprecated, but actually does nothing on newer versions of Android.

@ryanheise ryanheise pinned this issue May 31, 2023
@mikemcelligott
Copy link

Just a heads up that I'm getting OSStatus error -50 when attempting to use the code above (though it does compile and looks like it's trying to do the right thing). When I print out the list of devices on my iPhone I only get (id, name, type.name) the below (even though spotify is currently playing on a bluetooth speaker). I'm guessing the app has to break out to a native chooser to connect a bluetooth / airplay output to the current app or something, as suggested above.

flutter: iPhone Microphone, Built-In Microphone, builtInMic
flutter: Speaker, Speaker, builtInSpeaker

@mvn-hangtran-dn
Copy link

mvn-hangtran-dn commented Mar 1, 2024

@ryanheise I used AndroidAudioHardwareMode.inCommunication, but sometimes the text was not spoke fully on Android 13

@ryanheise
Copy link
Owner

Are you saying that it is routing to the correct hardware route but just that the audio is partial? I'm not sure if there is anything I can do in audio_session to address that.

@mvn-hangtran-dn
Copy link

@ryanheise Kindly testing functions of audio_session with new android versions(Android 13, 14) if you can. Many thanks!

@ryanheise
Copy link
Owner

I still don't understand what you mean since you didn't answer my question.

But aside from that, I don't actually see there is anything I can do in audio_session to address that. After all, the API you are using simply passes through to the operating system. The setMode method just passes through to the operating system, so that method does whatever the operating system does. I can't change what the operating system does.

@mvn-hangtran-dn
Copy link

@ryanheise
#39 (comment)
-> Yes, you got my problem correctly. It routes to the correct hardware route but just that the audio is partial.
Seem like setMode doesn't finish immediately, it need to a delay time.

@ryanheise
Copy link
Owner

Also don't forget to await the call to setMode():

await setMode(...);
await Future.delayed(Duration(seconds: 1)); // experiment as needed
await playSound();

@mvn-hangtran-dn
Copy link

await setMode(...); -> I always use await
await Future.delayed(Duration(seconds: 1)); -> I tried and the problem was fixed but the behaviour of my app is not good. For other android versions, I don't need it. I don't know why I need use this delay on Android 13.

@ryanheise
Copy link
Owner

This is the behaviour of the operating system. As suspected above, there isn't anything I can do to change the way the operating system works.

@zombie6888
Copy link

this works for me on ios:

Future<bool> switchToSpeaker() async {
    if (_androidAudioManager != null) {
      await _androidAudioManager.setMode(AndroidAudioHardwareMode.normal);
      await _androidAudioManager.stopBluetoothSco();
      await _androidAudioManager.setBluetoothScoOn(false);
      await _androidAudioManager.setSpeakerphoneOn(true);
    } else if (_avAudioSession != null) {
      await _avAudioSession
          .overrideOutputAudioPort(AVAudioSessionPortOverride.speaker);
    }
    return true;
  }

  Future<bool> switchToReceiver() async {
    if (_androidAudioManager != null) {
      await _androidAudioManager
          .setMode(AndroidAudioHardwareMode.inCommunication);
      await _androidAudioManager.stopBluetoothSco();
      await _androidAudioManager.setBluetoothScoOn(false);
      await _androidAudioManager.setSpeakerphoneOn(false);
      return true;
    } else if (_avAudioSession != null) {
      await _avAudioSession
          .overrideOutputAudioPort(AVAudioSessionPortOverride.none);
      return _switchToAnyIosPortIn({AVAudioSessionPort.builtInMic});
    }
    return false;
  } 

  Future<bool> switchToBluetooth() async {
    if (_androidAudioManager != null) {
      await _androidAudioManager
          .setMode(AndroidAudioHardwareMode.inCommunication);
      await _androidAudioManager.startBluetoothSco();
      await _androidAudioManager.setBluetoothScoOn(true);
      return true;
    } else if (_avAudioSession != null) {
      return _switchToAnyIosPortIn({
        AVAudioSessionPort.bluetoothLe,
        AVAudioSessionPort.bluetoothHfp,
        AVAudioSessionPort.bluetoothA2dp,
      });
    }
    return false;
  }

  Future<bool> _switchToAnyIosPortIn(Set<AVAudioSessionPort> ports) async {   
    for (final input in await _avAudioSession!.availableInputs) {
      if (ports.contains(input.portType)) {
        await _avAudioSession.setPreferredInput(input);
      }
    }
    return false;
  }

tested with this config.

 await _avAudioSession?.setCategory(
        AVAudioSessionCategory.playAndRecord,
        AVAudioSessionCategoryOptions.allowBluetooth,
        AVAudioSessionMode.spokenAudio,);

We are planning to use this plugin for video calls. We are tried to use flutter-webrtc which is also includes audio output changing functionality but this works much better and i found this plugin more flexible. There are a lot of non-obvious things in the ios avaudiosession api. For example, in some cases to change the output device you need to set preffered input device etc.

I will try Android part later and come back with feedback. Can we expect this functionality to be added to plugin if everything works?

@ryanheise
Copy link
Owner

Awesome! I appreciate your experimentation here, and yes I would definitely consider something like this being integrated into the main AudioSession class. On the Android side, there are new APIs for audio devices that I'll eventually need to get around to looking at, but for now we will have to go with the existing APIs in AudioManager (unless we run into a problem that can only be solved by implementing the new APIs).

@zombie6888
Copy link

The Android part also works. There are few cases that i haven't tested. These are different configs for ios, and usb/jack headphones, because i don't have such devices. I will let you know if I encounter any issues

@bradrf
Copy link

bradrf commented Aug 26, 2024

Has anyone tried the solution provided by @zombie6888 and @ryanheise (thank you!) and found that, on Android, it does indeed work, but something ends up changing the audio output behind the scenes? For example, I start out with "speaker" output, then change to "receiver", but after a short period (10 or 20 seconds, maybe?) it switches back to "speaker". Ideas on what might be going on?

@zombie6888
Copy link

Has anyone tried the solution provided by @zombie6888 and @ryanheise (thank you!) and found that, on Android, it does indeed work, but something ends up changing the audio output behind the scenes? For example, I start out with "speaker" output, then change to "receiver", but after a short period (10 or 20 seconds, maybe?) it switches back to "speaker". Ideas on what might be going on?

Do you have this permission in your AndroidManifest.xml?

<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

@bradrf
Copy link

bradrf commented Aug 28, 2024

Do you have this permission in your AndroidManifest.xml?

No, we don't have that permission, and, when added, doesn't change the behavior. We do have RECORD_AUDIO requested, though, so I suspect that might include the ability to modify audio settings, too, but don't know for sure.

FWIW, a workaround that seems to "fix" the issue is to always invoke the desired function to select output just before playing an audio file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants