@@ -292,6 +292,58 @@ class CallBloc extends Bloc<CallEvent, CallState> with WidgetsBindingObserver im
292292 if (change.nextState.activeCalls.length < change.currentState.activeCalls.length) {
293293 onCallEnded? .call ();
294294 }
295+
296+ /// Manages global side effects triggered by call lifecycle transitions.
297+ /// Key responsibility:
298+ /// - **iOS Audio Reset:** On the start of the *first* call, it forces the
299+ /// audio route back to the Receiver (Earpiece). This prevents the "sticky speaker"
300+ /// issue where iOS remembers the Speaker output from a previous session.
301+ _handleCallLifecycleTransitions (
302+ previousCalls: change.currentState.activeCalls,
303+ currentCalls: change.nextState.activeCalls,
304+ );
305+ }
306+
307+ /// Analyzes changes in the active call list to trigger specific lifecycle hooks.
308+ ///
309+ /// This method identifies keys transitions:
310+ /// * **First Call Started (`0 -> 1` ):** A cold start of the calling session.
311+ /// Crucial for initializing hardware resources (e.g., resetting speaker output on iOS).
312+ /// * **Last Call Ended (`N -> 0` ):** The termination of the calling session.
313+ /// Used for global cleanup and resource release.
314+ void _handleCallLifecycleTransitions ({
315+ required List <ActiveCall > previousCalls,
316+ required List <ActiveCall > currentCalls,
317+ }) {
318+ final wasEmpty = previousCalls.isEmpty;
319+ final isEmpty = currentCalls.isEmpty;
320+
321+ if (wasEmpty && ! isEmpty) {
322+ _onFirstCallStarted ();
323+ }
324+
325+ if (! wasEmpty && isEmpty) {
326+ _onLastCallEnded ();
327+ }
328+ }
329+
330+ /// Triggered when the first active call is established (0 -> 1 active calls).
331+ ///
332+ /// * **iOS:** Forces the audio output to the Receiver (Earpiece) via `Helper.setSpeakerphoneOn(false)` .
333+ /// This is a critical hard-reset to fix the "sticky speaker" issue where iOS
334+ /// retains the speaker route from a previous, unrelated session.
335+ void _onFirstCallStarted () {
336+ _logger.info (() => 'Lifecycle: First call started' );
337+ if (Platform .isIOS) Helper .setSpeakerphoneOn (false );
338+ }
339+
340+ /// Triggered when the last remaining active call ends (N -> 0 active calls).
341+ ///
342+ /// * **iOS:** Resets audio output to the default Receiver state to ensure clean state
343+ /// for future calls, preventing state bleeding between sessions.
344+ void _onLastCallEnded () {
345+ _logger.info (() => 'Lifecycle: Last call ended' );
346+ if (Platform .isIOS) Helper .setSpeakerphoneOn (false );
295347 }
296348
297349 void _handleSignalingSessionError ({required CallServiceState previous, required CallServiceState current}) {
0 commit comments