Skip to content

Commit

Permalink
feat!: Extend AudioContextConfig.duckAudio to `AudioContextConfig.f…
Browse files Browse the repository at this point in the history
…ocus` (#1720)

# Description

- Introduces `AudioContextConfigFocus.mixWithOthers` to unify
`AVAudioSessionOptions.mixWithOthers` (iOS) and `AndroidAudioFocus.none`
(Android)
- Replaces the option `AudioContextConfig.duckAudio = true` with
`AudioContextConfig.focus = AudioContextConfigFocus.duckOthers`

Closes #1718

### Migration instructions

Before:
```dart
AudioContextConfig(
  duckAudio: true,
);
```

After:
```dart
AudioContextConfig(
  focus: AudioContextConfigFocus.duckOthers,
);
```
  • Loading branch information
Gustl22 committed Dec 4, 2023
1 parent 3b81432 commit 87f3cb7
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 45 deletions.
12 changes: 7 additions & 5 deletions packages/audioplayers/example/lib/tabs/audio_context.dart
Expand Up @@ -123,11 +123,13 @@ class AudioContextTabState extends State<AudioContextTab>
audioContextConfig.copy(route: v),
),
),
Cbx(
'Duck Audio',
value: audioContextConfig.duckAudio,
({value}) => updateConfig(
audioContextConfig.copy(duckAudio: value),
LabeledDropDown<AudioContextConfigFocus>(
label: 'Audio Focus',
key: const Key('audioFocus'),
options: {for (final e in AudioContextConfigFocus.values) e: e.name},
selected: audioContextConfig.focus,
onChange: (v) => updateConfig(
audioContextConfig.copy(focus: v),
),
),
Cbx(
Expand Down
Expand Up @@ -22,29 +22,19 @@ class AudioContextConfig {

/// This flag determines how your audio interacts with other audio playing on
/// the device.
/// If your audio is playing, and another audio plays on top (like an alarm,
/// gps, etc), this determines what happens with your audio.
///
/// On Android, this will make an Audio Focus request with
/// AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK when your audio starts playing.
///
/// On iOS, this will set the option `.duckOthers` option
/// (the option `.mixWithOthers` is always set, regardless of these flags).
/// Note that, on iOS, this forces the category to be `.playAndRecord`, and
/// thus is forbidden when [respectSilence] is set.
final bool duckAudio;
final AudioContextConfigFocus focus;

/// Whether the "silent" mode of the device should be respected.
///
/// When `false` (the default), audio will be played even if the device is in
/// silent mode.
///
/// When `true` and the device is in silent mode, audio will not be played.
///
/// On Android, this will mandate the `USAGE_NOTIFICATION_RINGTONE` usage
/// type.
///
/// On iOS, setting this mandates the `.ambient` category, and it will be:
/// On iOS, setting this mandates the [AVAudioSessionCategory.ambient]
/// category, and it will be:
/// * silenced by rings
/// * silenced by the Silent switch
/// * silenced by screen locking (note: read [stayAwake] for details on
Expand All @@ -58,28 +48,28 @@ class AudioContextConfig {
/// On Android, this sets the player "wake mode" to `PARTIAL_WAKE_LOCK`.
///
/// On iOS, this will happen automatically as long as:
/// * the category is `.playAndRecord` (thus setting this is forbidden when
/// [respectSilence] is set)
/// * the category is [AVAudioSessionCategory.playAndRecord] (thus setting
/// this is forbidden when [respectSilence] is set)
/// * the UIBackgroundModes audio key has been added to your app’s
/// Info.plist (check our FAQ for more details on that)
final bool stayAwake;

AudioContextConfig({
this.route = AudioContextConfigRoute.system,
this.duckAudio = false,
this.focus = AudioContextConfigFocus.gain,
this.respectSilence = false,
this.stayAwake = false,
});

AudioContextConfig copy({
AudioContextConfigRoute? route,
bool? duckAudio,
AudioContextConfigFocus? focus,
bool? respectSilence,
bool? stayAwake,
}) {
return AudioContextConfig(
route: route ?? this.route,
duckAudio: duckAudio ?? this.duckAudio,
focus: focus ?? this.focus,
respectSilence: respectSilence ?? this.respectSilence,
stayAwake: stayAwake ?? this.stayAwake,
);
Expand All @@ -101,9 +91,11 @@ class AudioContextConfig {
: (route == AudioContextConfigRoute.earpiece
? AndroidUsageType.voiceCommunication
: AndroidUsageType.media),
audioFocus: duckAudio
? AndroidAudioFocus.gainTransientMayDuck
: AndroidAudioFocus.gain,
audioFocus: focus == AudioContextConfigFocus.gain
? AndroidAudioFocus.gain
: (focus == AudioContextConfigFocus.duckOthers
? AndroidAudioFocus.gainTransientMayDuck
: AndroidAudioFocus.none),
);
}

Expand All @@ -121,7 +113,10 @@ class AudioContextConfig {
? AVAudioSessionCategory.playAndRecord
: AVAudioSessionCategory.playback)),
options: {
if (duckAudio) AVAudioSessionOptions.duckOthers,
if (focus == AudioContextConfigFocus.duckOthers)
AVAudioSessionOptions.duckOthers,
if (focus == AudioContextConfigFocus.mixWithOthers)
AVAudioSessionOptions.mixWithOthers,
if (route == AudioContextConfigRoute.speaker)
AVAudioSessionOptions.defaultToSpeaker,
},
Expand All @@ -134,8 +129,12 @@ class AudioContextConfig {
const tip = 'Please create a custom [AudioContextIOS] if the generic flags '
'cannot represent your needs.';
assert(
!(duckAudio && respectSilence),
'$invalidMsg `respectSilence` and `duckAudio`. $tip',
!(respectSilence && focus == AudioContextConfigFocus.duckOthers),
'$invalidMsg `respectSilence` and `duckOthers`. $tip',
);
assert(
!(respectSilence && focus == AudioContextConfigFocus.mixWithOthers),
'$invalidMsg `respectSilence` and `mixWithOthers`. $tip',
);
assert(
!(respectSilence && route == AudioContextConfigRoute.speaker),
Expand All @@ -147,7 +146,7 @@ class AudioContextConfig {
String toString() {
return 'AudioContextConfig('
'route: $route, '
'duckAudio: $duckAudio, '
'focus: $focus, '
'respectSilence: $respectSilence, '
'stayAwake: $stayAwake'
')';
Expand All @@ -159,19 +158,48 @@ enum AudioContextConfigRoute {
/// earpiece, or a bluetooth device.
system,

/// On android, it will set `AndroidUsageType.voiceCommunication`.
/// On Android, this will set the usageType
/// [AndroidUsageType.voiceCommunication].
///
/// On iOS, it will set `AVAudioSessionCategory.playAndRecord`.
/// On iOS, this will set the category [AVAudioSessionCategory.playAndRecord].
earpiece,

/// On android, it will set `audioManager.isSpeakerphoneOn`.
/// On Android, this will set [AudioContextAndroid.isSpeakerphoneOn] to true.
///
/// On iOS, it will either:
/// On iOS, this will set the option [AVAudioSessionOptions.defaultToSpeaker].
/// Note that this forces the category to be
/// [AVAudioSessionCategory.playAndRecord], and thus is forbidden when
/// [AudioContextConfig.respectSilence] is set.
speaker,
}

enum AudioContextConfigFocus {
/// An option that expresses the fact that your application is
/// now the sole source of audio that the user is listening to.
///
/// * set the `.defaultToSpeaker` option OR
/// * call `overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)`
/// On Android, this will set the focus [AndroidAudioFocus.gain].
///
/// Note that, on iOS, this forces the category to be `.playAndRecord`, and
/// thus is forbidden when [AudioContextConfig.respectSilence] is set.
speaker,
/// On iOS, this will not set any additional [AVAudioSessionOptions].
gain,

/// An option that reduces the volume of other audio sessions while audio from
/// this session (like an alarm, gps, etc.) plays on top.
///
/// On Android, this will make an Audio Focus request with
/// [AndroidAudioFocus.gainTransientMayDuck] when your audio starts playing.
///
/// On iOS, this will set the option [AVAudioSessionOptions.duckOthers]
/// (the option [AVAudioSessionOptions.mixWithOthers] is set implicitly).
/// Note that this forces the category to be
/// [AVAudioSessionCategory.playAndRecord], and thus is forbidden when
/// [AudioContextConfig.respectSilence] is set.
duckOthers,

/// An option that indicates whether audio from this session mixes with audio
/// from active sessions in other audio apps.
///
/// On Android, this will set the focus [AndroidAudioFocus.none].
///
/// On iOS, this will set the option [AVAudioSessionOptions.mixWithOthers].
mixWithOthers,
}
Expand Up @@ -32,15 +32,16 @@ void main() {
test('Check AudioContextConfig assertions', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const boolValues = {true, false};
const focusValues = AudioContextConfigFocus.values;
const routeValues = AudioContextConfigRoute.values;

final throwsAssertion = [];
for (final isDuckAudio in boolValues) {
for (final focus in focusValues) {
for (final isRespectSilence in boolValues) {
for (final isStayAwake in boolValues) {
for (final route in routeValues) {
final config = AudioContextConfig(
duckAudio: isDuckAudio,
focus: focus,
respectSilence: isRespectSilence,
stayAwake: isStayAwake,
route: route,
Expand Down Expand Up @@ -69,6 +70,18 @@ void main() {
expect(
throwsAssertion,
const [
false,
false,
true,
false,
false,
true,
false,
false,
false,
false,
false,
false,
true,
true,
true,
Expand All @@ -81,11 +94,11 @@ void main() {
false,
false,
false,
false,
false,
true,
false,
false,
true,
true,
true,
true,
true,
false,
false,
Expand Down

0 comments on commit 87f3cb7

Please sign in to comment.