fix(ios-audio): keep session active when already-in-channel
The 'already in channel' server response (code 0x0302) is treated as a successful join by _onJoinChannel: the user stays in the channel and local state is updated to reflect the joined target. But the underlying voiceJoin call still raises BridgeError_ServerRejected, which the joinVoiceChannelWithIosAudioSession helper used to interpret as a join failure and deactivate the iOS audio session. Result: the UI shows the user as joined while the audio session is dead and capture/playback remain silent. Add an isJoinSuccess predicate to the ordering helper. When the predicate matches, the helper rethrows (so the caller can still run its success-on-already-joined branch) without deactivating the session. Wire _onJoinChannel to pass _isAlreadyInChannel as the predicate so the 0x0302 path keeps the session active. Adds two regression tests covering the success-on-rethrow and the predicate-false-still-deactivates paths.
This commit is contained in:
@@ -1308,6 +1308,11 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
|||||||
voiceJoin: rust.voiceJoin,
|
voiceJoin: rust.voiceJoin,
|
||||||
activateIosAudioSession: iosAudioSessionController.activate,
|
activateIosAudioSession: iosAudioSessionController.activate,
|
||||||
deactivateIosAudioSession: iosAudioSessionController.deactivate,
|
deactivateIosAudioSession: iosAudioSessionController.deactivate,
|
||||||
|
// Server says we are already in the target channel: the user is
|
||||||
|
// still joined to a voice channel, so the iOS audio session must
|
||||||
|
// stay active. The catch below converts this rethrow into the
|
||||||
|
// success-on-already-joined branch.
|
||||||
|
isJoinSuccess: _isAlreadyInChannel,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@@ -6,17 +6,30 @@ typedef VoiceJoinCallback = Future<void> Function({
|
|||||||
typedef IosVoiceSessionActivation = Future<void> Function();
|
typedef IosVoiceSessionActivation = Future<void> Function();
|
||||||
typedef IosVoiceSessionDeactivation = Future<void> Function();
|
typedef IosVoiceSessionDeactivation = Future<void> Function();
|
||||||
|
|
||||||
|
/// Predicate used to recognise `voiceJoin` errors that the caller treats as a
|
||||||
|
/// successful join outcome (e.g. the server replied "already in channel").
|
||||||
|
///
|
||||||
|
/// When this returns `true` for a thrown error, the iOS audio session is kept
|
||||||
|
/// active because the user is still considered joined to the channel. The
|
||||||
|
/// error is still rethrown so the caller can run its success-on-already-joined
|
||||||
|
/// branch and update local state.
|
||||||
|
typedef VoiceJoinSuccessPredicate = bool Function(Object error);
|
||||||
|
|
||||||
Future<void> joinVoiceChannelWithIosAudioSession({
|
Future<void> joinVoiceChannelWithIosAudioSession({
|
||||||
required BigInt channelId,
|
required BigInt channelId,
|
||||||
required String password,
|
required String password,
|
||||||
required VoiceJoinCallback voiceJoin,
|
required VoiceJoinCallback voiceJoin,
|
||||||
required IosVoiceSessionActivation activateIosAudioSession,
|
required IosVoiceSessionActivation activateIosAudioSession,
|
||||||
required IosVoiceSessionDeactivation deactivateIosAudioSession,
|
required IosVoiceSessionDeactivation deactivateIosAudioSession,
|
||||||
|
VoiceJoinSuccessPredicate? isJoinSuccess,
|
||||||
}) async {
|
}) async {
|
||||||
await activateIosAudioSession();
|
await activateIosAudioSession();
|
||||||
try {
|
try {
|
||||||
await voiceJoin(channelId: channelId, password: password);
|
await voiceJoin(channelId: channelId, password: password);
|
||||||
} catch (_) {
|
} catch (e) {
|
||||||
|
if (isJoinSuccess != null && isJoinSuccess(e)) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
await deactivateIosAudioSession();
|
await deactivateIosAudioSession();
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,5 +54,70 @@ void main() {
|
|||||||
'deactivateIosAudioSession',
|
'deactivateIosAudioSession',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'keeps the iOS audio session active when voiceJoin throws but the '
|
||||||
|
'error is recognised as already-in-channel (treated as success); '
|
||||||
|
'still rethrows so the caller runs its success-on-already-joined branch',
|
||||||
|
() async {
|
||||||
|
final calls = <String>[];
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
joinVoiceChannelWithIosAudioSession(
|
||||||
|
channelId: BigInt.from(42),
|
||||||
|
password: '',
|
||||||
|
activateIosAudioSession: () async {
|
||||||
|
calls.add('activateIosAudioSession');
|
||||||
|
},
|
||||||
|
deactivateIosAudioSession: () async {
|
||||||
|
calls.add('deactivateIosAudioSession');
|
||||||
|
},
|
||||||
|
voiceJoin: ({required channelId, required password}) async {
|
||||||
|
calls.add('voiceJoin');
|
||||||
|
throw _FakeAlreadyInChannel();
|
||||||
|
},
|
||||||
|
isJoinSuccess: (error) => error is _FakeAlreadyInChannel,
|
||||||
|
),
|
||||||
|
throwsA(isA<_FakeAlreadyInChannel>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(calls, ['activateIosAudioSession', 'voiceJoin']);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'deactivates the iOS audio session when isJoinSuccess returns false '
|
||||||
|
'for a non-success error',
|
||||||
|
() async {
|
||||||
|
final calls = <String>[];
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
joinVoiceChannelWithIosAudioSession(
|
||||||
|
channelId: BigInt.from(42),
|
||||||
|
password: '',
|
||||||
|
activateIosAudioSession: () async {
|
||||||
|
calls.add('activateIosAudioSession');
|
||||||
|
},
|
||||||
|
deactivateIosAudioSession: () async {
|
||||||
|
calls.add('deactivateIosAudioSession');
|
||||||
|
},
|
||||||
|
voiceJoin: ({required channelId, required password}) async {
|
||||||
|
calls.add('voiceJoin');
|
||||||
|
throw StateError('join rejected');
|
||||||
|
},
|
||||||
|
isJoinSuccess: (error) => error is _FakeAlreadyInChannel,
|
||||||
|
),
|
||||||
|
throwsStateError,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(calls, [
|
||||||
|
'activateIosAudioSession',
|
||||||
|
'voiceJoin',
|
||||||
|
'deactivateIosAudioSession',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FakeAlreadyInChannel implements Exception {}
|
||||||
|
|||||||
Reference in New Issue
Block a user