Skip to content

[Enhancement]Keep CallKit answer actions alive during WebRTC audio setup#1081

Merged
ipavlidakis merged 3 commits intodevelopfrom
iliaspavlidakis/ios-1477-improve-callkit-connectingjoining-state-by-moving-action
Mar 12, 2026
Merged

[Enhancement]Keep CallKit answer actions alive during WebRTC audio setup#1081
ipavlidakis merged 3 commits intodevelopfrom
iliaspavlidakis/ios-1477-improve-callkit-connectingjoining-state-by-moving-action

Conversation

@ipavlidakis
Copy link
Contributor

@ipavlidakis ipavlidakis commented Mar 11, 2026

🔗 Issue Links

Resolves https://linear.app/stream/issue/IOS-1477/improve-callkit-connectingjoining-state-by-moving-action-fulfilment

🎯 Goal

Ensure CallKit answer actions remain alive until WebRTC has configured the
audio device module, while documenting and covering the new join-source flow.

📝 Summary

  • carry a CallKit completion callback through JoinSource
  • complete that callback from WebRTCStateAdapter after the audio device
    module is configured

🛠 Implementation

CallKitService now stores a .callKit(.init { ... }) join source so the
answer action completion can travel with the join request. JoinSource
documents that payload and compares CallKit sources by a stable identifier so
tests and state comparisons can still work predictably.

WebRTCStateAdapter.configureAudioSession(source:) detects the CallKit payload
and completes it immediately after setAudioDeviceModule(...) succeeds. The
affected tests were updated to compare against a concrete JoinSource.callKit
value, and a dedicated adapter test now covers the completion callback path.

🎨 Showcase

N/A

Before After
N/A N/A

🧪 Manual Testing Notes

  • Accept an incoming CallKit call and verify audio becomes available after the
    call joins.

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change follows zero ⚠️ policy (required)
  • This change should receive manual QA
  • Changelog is updated with client-facing changes
  • New code is covered by unit tests
  • Comparison screenshots added for visual changes
  • Affected documentation updated (tutorial, CMS)

Summary by CodeRabbit

  • Bug Fixes

    • Improved CallKit integration so accepting incoming calls waits for the audio setup handoff before continuing, avoiding audio/session timing issues.
  • Tests

    • Added and updated tests to cover CallKit completion handling and audio session configuration flow.
  • Documentation

    • Added a changelog entry describing the CallKit/audio handoff fix.

@ipavlidakis ipavlidakis self-assigned this Mar 11, 2026
@ipavlidakis ipavlidakis requested a review from a team as a code owner March 11, 2026 14:13
@ipavlidakis ipavlidakis added bug Something isn't working enhancement New feature or request labels Mar 11, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e9743a1-eabd-4922-877c-e2f6b0e99fec

📥 Commits

Reviewing files that changed from the base of the PR and between 055ca0d and a139382.

📒 Files selected for processing (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

Adds a CallKit completion hook to the join flow: CallKit answer actions now receive an ActionCompletion that is invoked after WebRTC configures the audio device module, enabling explicit handoff of audio session ownership before continuing the join flow.

Changes

Cohort / File(s) Summary
Core Model Update
Sources/StreamVideo/Models/JoinSource.swift
JoinSource now conforms to Sendable, Equatable. Adds nested ActionCompletion (wraps a completion closure) and changes case callKit to callKit(ActionCompletion). Implements custom Equatable comparing CallKit by identifier.
Service Integration
Sources/StreamVideo/CallKit/CallKitService.swift
Replaces bare .callKit assignments with .callKit(.init { action.fulfill() }) in CXAnswerCallAction and initial join flow, passing a completion closure through the join source.
WebRTC Configuration
Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift
After audio device module setup, calls the CallKit completion for CallKit sources. Adjusts audio session activation so it is setActive only for .inApp joins (not CallKit).
Tests
StreamVideoTests/Call/Call_Tests.swift, StreamVideoTests/Controllers/CallController_Tests.swift, StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift, StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift
Updates tests to construct JoinSource.callKit(ActionCompletion) and assert accordingly. Adds test to verify CallKit completion is invoked during audio configuration.
Changelog
CHANGELOG.md
Adds entry noting CallKit answer action completion is kept alive until WebRTC configures the audio device module.

Sequence Diagram

sequenceDiagram
    participant CallKit as CallKit System
    participant Service as CallKitService
    participant Adapter as WebRTCStateAdapter
    participant Audio as Audio Device Module

    CallKit->>Service: Answer call action
    Service->>Service: Create ActionCompletion (action.fulfill)
    Service->>Service: Set joinSource = .callKit(ActionCompletion)
    Service->>Adapter: Start join / configure audio
    Adapter->>Audio: Configure audio device module
    Audio-->>Adapter: Configuration complete
    Adapter->>Service: Invoke ActionCompletion.complete()
    Service->>CallKit: Fulfill answer action
    CallKit->>CallKit: Release audio session
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A little hop, a tiny thunk,
CallKit waits while WebRTC cranks the trunk,
A closure passed, a gentle handoff done,
Audio flows right — now everyone's won! 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main enhancement: keeping CallKit answer actions alive during WebRTC audio setup, which aligns with the core changes across CallKitService, JoinSource, and WebRTCStateAdapter.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch iliaspavlidakis/ios-1477-improve-callkit-connectingjoining-state-by-moving-action

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Public Interface

🚀 No changes affecting the public interface.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/StreamVideo/CallKit/CallKitService.swift (1)

479-491: ⚠️ Potential issue | 🟠 Major

Complete the CXAnswerCallAction exactly once.

The callback introduced in the joinSource assignment already completes the answer action during audio session configuration. Line 486 still calls action.fulfill(), and line 491 can still call action.fail() after that callback has run. If join() completes after the audio-session handoff, the same action gets completed twice, violating CallKit's API contract. Remove the unconditional post-join action.fulfill() and guard the failure path so it only completes the action if the callback has not already done so.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/StreamVideo/CallKit/CallKitService.swift` around lines 479 - 491, The
answer action is being completed twice: once inside the joinSource callback and
again unconditionally after try await callToJoinEntry.call.join and in the catch
block; remove the unconditional action.fulfill() after the join and guard the
failure path so action.fail() is only called if the callback didn't already
complete the action. Implement this by introducing a local Bool (e.g.,
didCompleteAnswerAction = false) captured by the joinSource closure which sets
didCompleteAnswerAction = true when it calls action.fulfill(), then after try
await callToJoinEntry.call.join omit action.fulfill(), and in the catch block
call action.fail() only if didCompleteAnswerAction is false (also ensure you
still call callToJoinEntry.call.leave(), set(nil, for: action.callUUID), and
log.error(...) as before).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 17: The changelog entry "Ensure CallKit joins keep the answer action
completion alive until WebRTC has configured the audio device module." has a
mismatched PR reference: it is labeled `#1081` but the hyperlink points to
pull/1080; update the hyperlink target in CHANGELOG.md so the URL uses pull/1081
to match the label (or alternatively change the label to `#1080` if the intended
PR is 1080), ensuring the label and link are consistent.

---

Outside diff comments:
In `@Sources/StreamVideo/CallKit/CallKitService.swift`:
- Around line 479-491: The answer action is being completed twice: once inside
the joinSource callback and again unconditionally after try await
callToJoinEntry.call.join and in the catch block; remove the unconditional
action.fulfill() after the join and guard the failure path so action.fail() is
only called if the callback didn't already complete the action. Implement this
by introducing a local Bool (e.g., didCompleteAnswerAction = false) captured by
the joinSource closure which sets didCompleteAnswerAction = true when it calls
action.fulfill(), then after try await callToJoinEntry.call.join omit
action.fulfill(), and in the catch block call action.fail() only if
didCompleteAnswerAction is false (also ensure you still call
callToJoinEntry.call.leave(), set(nil, for: action.callUUID), and log.error(...)
as before).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31eba2dd-2dce-4c4c-aefd-52a55e8bd07f

📥 Commits

Reviewing files that changed from the base of the PR and between 007e729 and 055ca0d.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • Sources/StreamVideo/CallKit/CallKitService.swift
  • Sources/StreamVideo/Models/JoinSource.swift
  • Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift
  • StreamVideoTests/Call/Call_Tests.swift
  • StreamVideoTests/Controllers/CallController_Tests.swift
  • StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift
  • StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift

@ipavlidakis ipavlidakis enabled auto-merge (squash) March 12, 2026 11:30
@ipavlidakis ipavlidakis disabled auto-merge March 12, 2026 11:35
@ipavlidakis ipavlidakis merged commit 6b78beb into develop Mar 12, 2026
5 of 6 checks passed
@ipavlidakis ipavlidakis deleted the iliaspavlidakis/ios-1477-improve-callkit-connectingjoining-state-by-moving-action branch March 12, 2026 11:35
@Stream-SDK-Bot
Copy link
Collaborator

SDK Size

title develop branch diff status
StreamVideo 10.07 MB 10.08 MB +17 KB 🟢
StreamVideoSwiftUI 2.45 MB 2.45 MB 0 KB 🟢
StreamVideoUIKit 2.58 MB 2.58 MB 0 KB 🟢
StreamWebRTC 11.09 MB 11.09 MB 0 KB 🟢

@Stream-SDK-Bot
Copy link
Collaborator

StreamVideo XCSize

Object Diff (bytes)
WebRTCStateAdapter.o +6010
JoinSource.o +3611
CallAudioSession.o -2953
Call+Stage.o +2744
WebRTCCoordinator+Stage.o +2044
Show 8 more objects
Object Diff (bytes)
CallKitService.o +824
Call.o +788
WebRTCCoordinator+Joining.o +176
StreamVideoProcessPipeline.o -152
CallController.o +100
StreamVideoEnvironment.o +92
CoordinatorModels.o +80
CallState.o +76

@sonarqubecloud
Copy link

This was referenced Mar 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants