Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/audioplayers/example/lib/tabs/audio_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class AudioContextTabState extends State<AudioContextTab>
AudioContextConfig audioContextConfig = AudioContextConfig();

/// Set config for each platform individually
AudioContext audioContext = const AudioContext();
AudioContext audioContext = AudioContext();

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -194,7 +194,7 @@ class AudioContextTabState extends State<AudioContextTab>
Widget _iosTab() {
final iosOptions = AVAudioSessionOptions.values.map(
(option) {
final options = audioContext.iOS.options.toList();
final options = audioContext.iOS.options;
return Cbx(
option.name,
value: options.contains(option),
Expand Down
8 changes: 4 additions & 4 deletions packages/audioplayers/test/global_audioplayers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ void main() {
/// If using [AVAudioSessionCategory.playAndRecord] the audio will come from
/// the earpiece unless [AVAudioSessionOptions.defaultToSpeaker] is used.
test('set AudioContext', () async {
await globalScope.setAudioContext(const AudioContext());
await globalScope.setAudioContext(AudioContext());
final call = globalPlatform.popLastCall();
expect(call.method, 'setGlobalAudioContext');
expect(
call.value,
const AudioContext(
android: AudioContextAndroid(
AudioContext(
android: const AudioContextAndroid(
isSpeakerphoneOn: false,
audioMode: AndroidAudioMode.normal,
stayAwake: false,
Expand All @@ -49,7 +49,7 @@ void main() {
),
iOS: AudioContextIOS(
category: AVAudioSessionCategory.playback,
options: [],
options: const {},
),
),
);
Expand Down
166 changes: 156 additions & 10 deletions packages/audioplayers_platform_interface/lib/src/api/audio_context.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';

/// An Audio Context is a set of secondary, platform-specific aspects of audio
/// playback, typically related to how the act of playing audio interacts with
/// other features of the device. [AudioContext] is containing platform specific
/// configurations: [AudioContextAndroid] and [AudioContextIOS].
@immutable
class AudioContext {
final AudioContextAndroid android;
final AudioContextIOS iOS;
late final AudioContextIOS iOS;

const AudioContext({
this.android = const AudioContextAndroid(),
this.iOS = const AudioContextIOS(),
});
AudioContext({
AudioContextAndroid? android,
AudioContextIOS? iOS,
}) : android = android ?? const AudioContextAndroid() {
this.iOS = iOS ?? AudioContextIOS();
}

AudioContext copy({
AudioContextAndroid? android,
Expand All @@ -36,10 +40,34 @@ class AudioContext {
return <String, dynamic>{};
}
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
other is AudioContext &&
runtimeType == other.runtimeType &&
android == other.android &&
iOS == other.iOS;
}

@override
int get hashCode => Object.hash(
android,
iOS,
);

@override
String toString() {
return 'AudioContext('
'android: $android, '
'iOS: $iOS'
')';
}
}

/// A platform-specific class to encapsulate a collection of attributes about an
/// Android audio stream.
@immutable
class AudioContextAndroid {
/// Sets the speakerphone on or off, globally.
///
Expand Down Expand Up @@ -98,23 +126,118 @@ class AudioContextAndroid {
'audioFocus': audioFocus.value,
};
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
other is AudioContextAndroid &&
runtimeType == other.runtimeType &&
isSpeakerphoneOn == other.isSpeakerphoneOn &&
audioMode == other.audioMode &&
stayAwake == other.stayAwake &&
contentType == other.contentType &&
usageType == other.usageType &&
audioFocus == other.audioFocus;
}

@override
int get hashCode => Object.hash(
isSpeakerphoneOn,
audioMode,
stayAwake,
contentType,
usageType,
audioFocus,
);

@override
String toString() {
return 'AudioContextAndroid('
'isSpeakerphoneOn: $isSpeakerphoneOn, '
'audioMode: $audioMode, '
'stayAwake: $stayAwake, '
'contentType: $contentType, '
'usageType: $usageType, '
'audioFocus: $audioFocus'
')';
}
}

/// A platform-specific class to encapsulate a collection of attributes about an
/// iOS audio stream.
@immutable
class AudioContextIOS {
final AVAudioSessionCategory category;
final List<AVAudioSessionOptions> options;
final Set<AVAudioSessionOptions> options;

// Note when changing the defaults, it should also be changed in native code.
const AudioContextIOS({
AudioContextIOS({
Comment thread
spydon marked this conversation as resolved.
this.category = AVAudioSessionCategory.playback,
this.options = const [],
});
this.options = const {},
}) : assert(
category == AVAudioSessionCategory.playback ||
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(AVAudioSessionOptions.mixWithOthers),
'You can set the option `mixWithOthers` explicitly only if the '
'audio session category is `playAndRecord`, `playback`, or '
'`multiRoute`.'),
assert(
category == AVAudioSessionCategory.playback ||
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(AVAudioSessionOptions.duckOthers),
'You can set the option `duckOthers` explicitly only if the audio '
'session category is `playAndRecord`, `playback`, or `multiRoute`.',
),
assert(
category == AVAudioSessionCategory.playback ||
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(
AVAudioSessionOptions.interruptSpokenAudioAndMixWithOthers,
),
'You can set the option `interruptSpokenAudioAndMixWithOthers` '
'explicitly only if the audio session category is `playAndRecord`, '
'`playback`, or `multiRoute`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.record ||
!options.contains(AVAudioSessionOptions.allowBluetooth),
'You can set the option `allowBluetooth` explicitly only if the '
'audio session category is `playAndRecord` or `record`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.record ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(AVAudioSessionOptions.allowBluetoothA2DP),
'You can set the option `allowBluetoothA2DP` explicitly only if '
'the audio session category is `playAndRecord`, `record`, or '
'`multiRoute`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
!options.contains(AVAudioSessionOptions.allowAirPlay),
'You can set the option `allowAirPlay` explicitly only if the '
'audio session category is `playAndRecord`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
!options.contains(AVAudioSessionOptions.defaultToSpeaker),
'You can set the option `defaultToSpeaker` explicitly only if the '
'audio session category is `playAndRecord`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.record ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(
AVAudioSessionOptions.overrideMutedMicrophoneInterruption,
),
'You can set the option `overrideMutedMicrophoneInterruption` '
'explicitly only if the audio session category is `playAndRecord`, '
'`record`, or `multiRoute`.');

AudioContextIOS copy({
AVAudioSessionCategory? category,
List<AVAudioSessionOptions>? options,
Set<AVAudioSessionOptions>? options,
}) {
return AudioContextIOS(
category: category ?? this.category,
Expand All @@ -128,6 +251,29 @@ class AudioContextIOS {
'options': options.map((e) => e.name).toList(),
};
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
other is AudioContextIOS &&
runtimeType == other.runtimeType &&
category == other.category &&
const SetEquality().equals(options, other.options);
}

@override
int get hashCode => Object.hash(
category,
options,
);

@override
String toString() {
return 'AudioContextIOS('
'category: $category, '
'options: $options'
')';
}
}

/// "what" you are playing. The content type expresses the general category of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,20 @@ class AudioContextConfig {
: (route == AudioContextConfigRoute.earpiece
? AVAudioSessionCategory.playAndRecord
: AVAudioSessionCategory.playback)),
options: (duckAudio
? [AVAudioSessionOptions.duckOthers]
: <AVAudioSessionOptions>[]) +
(route == AudioContextConfigRoute.speaker
? [AVAudioSessionOptions.defaultToSpeaker]
: []),
options: {
if (duckAudio) AVAudioSessionOptions.duckOthers,
if (route == AudioContextConfigRoute.speaker)
AVAudioSessionOptions.defaultToSpeaker,
},
);
}

void validateIOS() {
// Please create a custom [AudioContextIOS] if the generic flags cannot
// represent your needs.
if (respectSilence && route == AudioContextConfigRoute.speaker) {
throw 'On iOS it is impossible to set both respectSilence and '
'forceSpeaker';
throw 'On iOS it is impossible to set both `respectSilence` and route '
'`speaker`';
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/audioplayers_platform_interface/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ homepage: https://github.com/bluefireteam/audioplayers
repository: https://github.com/bluefireteam/audioplayers/tree/master/packages/audioplayers_platform_interface

dependencies:
collection: ^1.17.1
flutter:
sdk: flutter
meta: ^1.7.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//ignore_for_file: avoid_redundant_argument_values

import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

test('Create default AudioContext', () async {
final context = AudioContext();
expect(
context,
AudioContext(
android: const AudioContextAndroid(
isSpeakerphoneOn: false,
audioMode: AndroidAudioMode.normal,
stayAwake: false,
contentType: AndroidContentType.music,
usageType: AndroidUsageType.media,
audioFocus: AndroidAudioFocus.gain,
),
iOS: AudioContextIOS(
category: AVAudioSessionCategory.playback,
options: const {},
),
),
);
});

test('Create invalid AudioContextIOS', () async {
try {
// Throws AssertionError:
AudioContextIOS(
category: AVAudioSessionCategory.ambient,
options: const {AVAudioSessionOptions.mixWithOthers},
);
fail('AssertionError not thrown');
// ignore: avoid_catches_without_on_clauses
} catch (e) {
expect(e, isInstanceOf<AssertionError>());
expect(
(e as AssertionError).message,
'You can set the option `mixWithOthers` explicitly only if the audio '
'session category is `playAndRecord`, `playback`, or `multiRoute`.');
}
});

test('Equality of AudioContextIOS', () async {
final context1 = AudioContextIOS(
category: AVAudioSessionCategory.playAndRecord,
options: const {
AVAudioSessionOptions.mixWithOthers,
AVAudioSessionOptions.defaultToSpeaker,
},
);
final context2 = AudioContextIOS(
category: AVAudioSessionCategory.playAndRecord,
options: const {
AVAudioSessionOptions.defaultToSpeaker,
AVAudioSessionOptions.mixWithOthers,
},
);
expect(context1, context2);
});
}
Loading