diff --git a/lib/features/video_calls/managers/video_call_manager.dart b/lib/features/video_calls/managers/video_call_manager.dart index 91aed3a1..36962209 100644 --- a/lib/features/video_calls/managers/video_call_manager.dart +++ b/lib/features/video_calls/managers/video_call_manager.dart @@ -592,6 +592,7 @@ class VideoCallManager { ); if (next) { await _audioSession.restartCapture(); + await _bindIosCallKitAudioEngine(); } } catch (_) { return; @@ -791,6 +792,7 @@ class VideoCallManager { audioCaptureOptions: _micCaptureOptions, ); await _audioSession.restartCapture(); + await _bindIosCallKitAudioEngine(); } catch (_) {} } await _resumeVideoAfterForeground(); @@ -1264,6 +1266,7 @@ class VideoCallManager { audioCaptureOptions: _micCaptureOptions, ); await _audioSession.restartCapture(); + await _bindIosCallKitAudioEngine(); } catch (_) { effectiveMic = false; } @@ -1304,6 +1307,17 @@ class VideoCallManager { _lastMutedMicTotalEnergy = null; } + Future _bindIosCallKitAudioEngine() async { + if (!Platform.isIOS) return; + try { + final calls = await FlutterCallkitIncoming.activeCalls(); + if (calls.isEmpty) return; + } catch (_) { + return; + } + await _audioSession.startCallKitCapture(); + } + Future _startCallAudioSession() async { if (!_canHandleCallAudio) return; try { diff --git a/lib/features/video_calls/services/video_call_audio_session_coordinator.dart b/lib/features/video_calls/services/video_call_audio_session_coordinator.dart index e08bb8e8..c65040ac 100644 --- a/lib/features/video_calls/services/video_call_audio_session_coordinator.dart +++ b/lib/features/video_calls/services/video_call_audio_session_coordinator.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; typedef VideoCallAudioSessionFactory = Future Function(); typedef IosCallAudioSessionHandler = Future Function(String method); +typedef IosAdmHandler = Future Function(); /// Configures and owns the platform audio session for an active VoIP call. class VideoCallAudioSessionCoordinator { @@ -12,18 +13,27 @@ class VideoCallAudioSessionCoordinator { VideoCallAudioSessionFactory? audioSessionFactory, bool? isIos, IosCallAudioSessionHandler? iosCallAudioSession, + IosAdmHandler? stopLocalRecording, + IosAdmHandler? startLocalRecording, }) : _audioSessionFactory = audioSessionFactory ?? (() => AudioSession.instance), _isIos = isIos ?? Platform.isIOS, - _iosCallAudioSession = iosCallAudioSession ?? _invokeIosCallAudioSession; + _iosCallAudioSession = iosCallAudioSession ?? _invokeIosCallAudioSession, + _stopLocalRecording = stopLocalRecording ?? _invokeStopLocalRecording, + _startLocalRecording = startLocalRecording ?? _invokeStartLocalRecording; static const MethodChannel _iosChannel = MethodChannel( 'com.eatvioapp/call_audio_session', ); + static const MethodChannel _webrtcChannel = MethodChannel( + 'FlutterWebRTC.Method', + ); final VideoCallAudioSessionFactory _audioSessionFactory; final bool _isIos; final IosCallAudioSessionHandler _iosCallAudioSession; + final IosAdmHandler _stopLocalRecording; + final IosAdmHandler _startLocalRecording; static final AudioSessionConfiguration callConfiguration = AudioSessionConfiguration( @@ -54,6 +64,14 @@ class VideoCallAudioSessionCoordinator { return _iosChannel.invokeMethod(method); } + static Future _invokeStopLocalRecording() { + return _webrtcChannel.invokeMethod('stopLocalRecording'); + } + + static Future _invokeStartLocalRecording() { + return _webrtcChannel.invokeMethod('startLocalRecording'); + } + Future start() async { if (_isIos) { await _iosCallAudioSession('activate'); @@ -97,6 +115,21 @@ class VideoCallAudioSessionCoordinator { } catch (_) {} } + /// flutter-webrtc 1.2 uses AudioEngine ADM. `isAudioEnabled` does not start + /// that engine. Incoming CallKit therefore needs `initAndStartRecording` + /// after the microphone track exists and CallKit has activated the session. + Future startCallKitCapture() async { + if (!_isIos) return; + try { + await _stopLocalRecording(); + } catch (_) {} + await Future.delayed(const Duration(milliseconds: 120)); + try { + await _startLocalRecording(); + _iosActive = true; + } catch (_) {} + } + Future stop() async { if (_isIos) { _iosActive = false; diff --git a/pubspec.yaml b/pubspec.yaml index 84090d9a..0f2c7383 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.63+152 +version: 1.3.64+153 publish_to: 'none' environment: diff --git a/test/features/video_calls/services/video_call_audio_session_coordinator_test.dart b/test/features/video_calls/services/video_call_audio_session_coordinator_test.dart index 71b266f6..1621785c 100644 --- a/test/features/video_calls/services/video_call_audio_session_coordinator_test.dart +++ b/test/features/video_calls/services/video_call_audio_session_coordinator_test.dart @@ -28,6 +28,21 @@ void main() { expect(calls, ['activate', 'activate', 'restart', 'deactivate']); }); + test('startCallKitCapture restarts the AudioEngine ADM', () async { + final adm = []; + final coordinator = VideoCallAudioSessionCoordinator( + isIos: true, + iosCallAudioSession: (_) async {}, + stopLocalRecording: () async => adm.add('stop'), + startLocalRecording: () async => adm.add('start'), + ); + + await coordinator.startCallKitCapture(); + + expect(coordinator.isActive, isTrue); + expect(adm, ['stop', 'start']); + }); + test('stop clears the active session when deactivate fails', () async { final coordinator = VideoCallAudioSessionCoordinator( isIos: true,