Open mirkancal opened 3 years ago
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.)
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.
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:
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.
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.
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
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 ).
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.
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
@ryanheise I used AndroidAudioHardwareMode.inCommunication
, but sometimes the text was not spoke fully on Android 13
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.
@ryanheise Kindly testing functions of audio_session with new android versions(Android 13, 14) if you can. Many thanks!
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.
@ryanheise
https://github.com/ryanheise/audio_session/issues/39#issuecomment-1972472162
-> 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.
Also don't forget to await the call to setMode()
:
await setMode(...);
await Future.delayed(Duration(seconds: 1)); // experiment as needed
await playSound();
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.
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.
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?
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).
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
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?
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" />
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.
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?