Skip to content

Implemented LiveCommunicationKit as an alternative to CallKit#1200

Draft
martinmitrevski wants to merge 1 commit into
developfrom
live-communication-kit
Draft

Implemented LiveCommunicationKit as an alternative to CallKit#1200
martinmitrevski wants to merge 1 commit into
developfrom
live-communication-kit

Conversation

@martinmitrevski

@martinmitrevski martinmitrevski commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🔗 Issue Links

Provide all JIRA tickets and/or GitHub issues related to this PR, if applicable.

🎯 Goal

Describe why we are making this change.

📝 Summary

Provide bullet points with the most important changes in the codebase.

🛠 Implementation

Provide a detailed description of the implementation and explain your decisions if you find them relevant.

🎨 Showcase

Add relevant screenshots and/or videos/gifs to easily see what this PR changes, if applicable.

Before After
img img

🧪 Manual Testing Notes

Explain how this change can be tested manually, if applicable.

☑️ 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)

🎁 Meme

Provide a funny gif or image that relates to your work on this pull request. (Optional)

Summary by CodeRabbit

  • New Features

    • Added LiveCommunicationKit support for incoming and ongoing calls on iOS 27+, with an option to continue using CallKit.
    • Added configuration to choose the system calling framework.
  • Bug Fixes

    • Improved reliability when handling call notifications, reconnections, muting, and call completion.
    • Prevented invalid audio formats from disrupting audio setup.
    • Improved call sound playback synchronization.
  • Compatibility

    • Updated the minimum supported iOS version to 15.
    • Simplified call and permissions interfaces across supported iOS versions.

@martinmitrevski
martinmitrevski requested a review from a team as a code owner July 16, 2026 14:44
@martinmitrevski
martinmitrevski marked this pull request as draft July 16, 2026 14:44
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds LiveCommunicationKit integration with configurable CallKit routing, hardens audio-format and playback handling, removes iOS 13 SwiftUI compatibility code, raises deployment targets to iOS 15, and updates related tests and project schemes.

Changes

LiveCommunicationKit calling

Layer / File(s) Summary
Calling contract and service implementation
Sources/StreamVideo/CallKit/SystemCallingService.swift, Sources/StreamVideo/CallKit/LiveCommunicationKitService.swift, Sources/StreamVideo/VideoConfig.swift
Adds the shared calling-service interface, LiveCommunicationKit lifecycle implementation, dependency injection, and useLiveCommunicationKit configuration.
Calling-service routing
Sources/StreamVideo/CallKit/CallKitAdapter.swift, Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift, Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift
Routes configuration, incoming pushes, completions, and reconnection checks through the selected calling service.
Calling integration tests
StreamVideoTests/CallKit/*, StreamVideoTests/Mock/VideoConfig+Dummy.swift
Adds LiveCommunicationKit mocks and verifies service selection, incoming-call reporting, configuration defaults, and lifecycle state.

Audio playback and validation

Layer / File(s) Summary
Audio-format validation
Sources/StreamVideo/Utils/AudioSession/AudioDeviceModule/*, StreamVideoTests/Utils/AudioSession/AudioDeviceModule/*
Rejects invalid audio formats before installing taps or modifying engine connections, with tests for clearing and later valid configuration.
Serialized call sounds
Sources/StreamVideoSwiftUI/Utils/CallSoundsPlayer.swift, DemoApp/Sources/Components/AudioTrackPlayer/AudioTrackPlayer.swift
Serializes call-sound operations through an actor and removes automatic stopping from the demo audio player delegate callback.

iOS 15 SwiftUI platform cleanup

Layer / File(s) Summary
SwiftUI availability and presentation
Sources/StreamVideoSwiftUI/CallView/*, Sources/StreamVideoSwiftUI/CallView/ViewModifiers/*, Sources/StreamVideoSwiftUI/ViewFactory.swift, Sources/StreamVideoSwiftUI/CallingViews/iOS13/*
Removes iOS 13 backports, simplifies availability branches, updates call-ended sheet handling, and adds a preconcurrency conformance.
Project configuration
StreamVideo.xcodeproj/project.pbxproj, StreamVideo.xcodeproj/xcshareddata/xcschemes/*
Sets deployment targets to iOS 15, removes deleted compatibility-file exceptions, and disables Metal API validation in shared schemes.
Concurrency isolation
DemoApp/Sources/Components/Debug/Items/FeatureFlags/DebugMenu+FeatureFlagsView.swift
Marks the feature-flag collection as main-actor isolated.

Test access robustness

Layer / File(s) Summary
WebRTC mock access helpers
StreamVideoTests/WebRTC/SFU/*, StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift
Centralizes publisher and subscriber mock retrieval and adds earlier XCTest unwrapping for required state.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement, bug

Suggested reviewers: ipavlidakis

Sequence Diagram(s)

sequenceDiagram
  participant PushNotificationAdapter
  participant CallKitAdapter
  participant LiveCommunicationKitService
  participant StreamVideo
  PushNotificationAdapter->>CallKitAdapter: receive VoIP push
  CallKitAdapter->>LiveCommunicationKitService: reportIncomingCall
  LiveCommunicationKitService->>LiveCommunicationKitService: report conversation and prepare call
  LiveCommunicationKitService->>StreamVideo: ensure connection
  LiveCommunicationKitService->>LiveCommunicationKitService: handle join, end, or mute action
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding LiveCommunicationKit as an alternative to CallKit.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch live-communication-kit

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.

@github-actions

Copy link
Copy Markdown
1 Message
📖 Skipping Danger since the Pull Request is classed as Draft/Work In Progress

Generated by 🚫 Danger

@github-actions

Copy link
Copy Markdown

Public Interface

+ @available(iOS 27.0, *) extension LiveCommunicationKitService: InjectionKey  
+ 
+   public nonisolated static var currentValue: LiveCommunicationKitService

+ @available(iOS 27.0, *) open class LiveCommunicationKitService: NSObject, ConversationManagerDelegate, SystemCallingService, @unchecked Sendable  
+ 
+   public var streamVideo: StreamVideo?
+   public let eventPipeline: AnyPublisher<CallKitService.Event, Never>
+   open var callId: String
+   open var callType: String
+   open var iconTemplateImageData: Data?
+   open var ringtoneSound: String?
+   open var supportsHolding: Bool
+   open var supportsVideo: Bool
+   open var includesCallsInRecents: Bool
+   open var missingPermissionPolicy: CallKitMissingPermissionPolicy
+   open var participantAutoLeavePolicy: ParticipantAutoLeavePolicy
+   public var callJoinInterceptor: CallJoinIntercepting?
+   open internal lazy var conversationManager
+   
+ 
+   override public init()
+   
+ 
+   open func reportIncomingCall(_ cid: String,localizedCallerName: String,callerId: String,hasVideo: Bool = false,completion: @Sendable @escaping (Error?) -> Void)
+   open func callAccepted(_ response: CallAcceptedEvent)
+   open func callRejected(_ response: CallRejectedEvent)
+   open func callEnded(_ cId: String,ringingTimedOut: Bool,leaveReason: String? = nil)
+   open func callParticipantLeft(_ response: CallSessionParticipantLeftEvent)
+   open func conversationManagerDidBegin(_ manager: ConversationManager)
+   open func conversationManagerDidReset(_ manager: ConversationManager)
+   open func conversationManager(_ manager: ConversationManager,conversationChanged conversation: Conversation)
+   open func conversationManager(_ manager: ConversationManager,didActivate audioSession: AVAudioSession)
+   public func conversationManager(_ manager: ConversationManager,didDeactivate audioSession: AVAudioSession)
+   open func conversationManager(_ manager: ConversationManager,perform action: ConversationAction)
+   open func conversationManager(_ manager: ConversationManager,timedOutPerforming action: ConversationAction)
+   open func requestTransaction(_ action: ConversationAction)async throws 
+   open func checkIfCallWasHandled(callState: GetCallResponse)-> String?
+   open func setUpRingingTimer(for callState: GetCallResponse)
+   open func didUpdate(_ streamVideo: StreamVideo?)

- @available(iOS, introduced: 13, obsoleted: 14) public struct LobbyView_iOS13: View  
- 
-   public var body: some View
-   
- 
-   public init(viewFactory: Factory = DefaultViewFactory.shared,callViewModel: CallViewModel,callId: String,callType: String,callSettings: Binding<CallSettings>,onJoinCallTap: @escaping () -> Void,onCloseLobby: @escaping () -> Void)

- @propertyWrapper @available(iOS, introduced: 13, obsoleted: 14) public struct PublishedObject  
- 
-   public var wrappedValue: Value
-   public var projectedValue: Publisher
-   
- 
-   public static subscript<EnclosingSelf: ObservableObject & Sendable>(_enclosingInstance observed: EnclosingSelf,wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedObject>)-> Value where EnclosingSelf.ObjectWillChangePublisher == ObservableObjectPublisher
-   public static subscript<EnclosingSelf: ObservableObject & Sendable>(_enclosingInstance observed: EnclosingSelf,projected wrappedKeyPath: KeyPath<EnclosingSelf, Publisher>,storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedObject>)-> Publisher where EnclosingSelf.ObjectWillChangePublisher == ObservableObjectPublisher
-   
- 
-   public init(wrappedValue: Value)
-   public init(wrappedValue: V?)

- @available(iOS, introduced: 13, obsoleted: 14) public struct IncomingCallView_iOS13: View  
- 
-   public var body: some View
-   
- 
-   public init(viewFactory: Factory = DefaultViewFactory.shared,callInfo: IncomingCall,onCallAccepted: @escaping (String) -> Void,onCallRejected: @escaping (String) -> Void)

- @available(iOS, introduced: 13, obsoleted: 14) public struct LocalParticipantViewModifier_iOS13: ViewModifier  
- 
-   public func body(content: Content)-> some View

- @available(iOS, introduced: 13, obsoleted: 14) public struct CallContainer_iOS13: View  
- 
-   public var body: some View
-   
- 
-   public init(viewFactory: Factory = DefaultViewFactory.shared,viewModel: CallViewModel)

- @MainActor @propertyWrapper @available(iOS, introduced: 13, obsoleted: 14) public final class BackportStateObject: DynamicProperty, @unchecked Sendable  
- 
-   public var wrappedValue: ObjectType
-   public var projectedValue: ObservedObject<ObjectType>.Wrapper
-   
- 
-   public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
-   
- 
-   public nonisolated func update()

 public final class VideoConfig: Sendable  
-   
+   public let useLiveCommunicationKit: Bool
- 
+   
-   public init(videoFilters: [VideoFilter] = [],noiseCancellationFilter: NoiseCancellationFilter? = nil,audioProcessingModule: AudioProcessingModule? = nil,usesProcessingPipeline: Bool = true,usesNewCapturingPipeline: Bool = true)
+ 
+   public init(videoFilters: [VideoFilter] = [],noiseCancellationFilter: NoiseCancellationFilter? = nil,audioProcessingModule: AudioProcessingModule? = nil,usesProcessingPipeline: Bool = true,usesNewCapturingPipeline: Bool = true,useLiveCommunicationKit: Bool = true)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
DemoApp/Sources/Components/AudioTrackPlayer/AudioTrackPlayer.swift (1)

31-46: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Avoid main-thread file I/O during audio player initialization.

The AVAudioPlayer(contentsOf:) initializer reads the file header from disk, which can block the main thread and cause frame drops. As per coding guidelines, avoid heavy work on the main thread to keep rendering efficient.

Refactor the operation to perform the file I/O and initialization on the background thread, and then hop to the MainActor only for state updates and playback control.

⚡ Proposed refactor
-        processingQueue.addTaskOperation { `@MainActor` [weak self] in
-            guard
-                let self,
-                self.track != track,
-                let url = Bundle.main.url(forResource: track.rawValue, withExtension: track.fileExtension),
-                let audioPlayer = try? AVAudioPlayer(contentsOf: url)
-            else {
-                return
-            }
-
-            self.audioPlayer = audioPlayer
-            audioPlayer.play()
-            audioPlayer.numberOfLoops = 1000
-            self.track = track
-            self.isPlaying = true
-        }
+        processingQueue.addTaskOperation { [weak self] in
+            guard let self else { return }
+            
+            let isSameTrack = await MainActor.run { self.track == track }
+            guard !isSameTrack,
+                  let url = Bundle.main.url(forResource: track.rawValue, withExtension: track.fileExtension),
+                  let audioPlayer = try? AVAudioPlayer(contentsOf: url)
+            else {
+                return
+            }
+
+            audioPlayer.numberOfLoops = 1000
+            
+            await MainActor.run {
+                self.audioPlayer?.stop()
+                self.audioPlayer = audioPlayer
+                audioPlayer.play()
+                self.track = track
+                self.isPlaying = true
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DemoApp/Sources/Components/AudioTrackPlayer/AudioTrackPlayer.swift` around
lines 31 - 46, Refactor the processingQueue task around AudioTrackPlayer so
Bundle URL lookup and AVAudioPlayer(contentsOf:) initialization occur off the
MainActor. After initialization completes, hop to MainActor for the existing
track validation, audioPlayer assignment, playback, loop count, and isPlaying
updates, preserving the weak-self and early-return behavior.

Source: Coding guidelines

🧹 Nitpick comments (2)
StreamVideoTests/VideoConfig_Tests.swift (1)

10-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align test names with the required convention.

Both method names include when; rename them to omit given, when, and then while preserving the scenario/result structure.

Suggested rename
-    func test_init_whenUseLiveCommunicationKitIsNotProvided_defaultsToTrue() {
+    func test_init_useLiveCommunicationKitNotProvided_defaultsToTrue() {
...
-    func test_init_whenUseLiveCommunicationKitIsFalse_setsValue() {
+    func test_init_useLiveCommunicationKitFalse_setsValue() {

As per coding guidelines, test methods must omit the words given, when, and then from the method name itself.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@StreamVideoTests/VideoConfig_Tests.swift` around lines 10 - 19, Rename the
two VideoConfig test methods to remove “when” while preserving their scenario
and expected-result meaning:
test_init_useLiveCommunicationKitNotProvided_defaultsToTrue and
test_init_useLiveCommunicationKitFalse_setsValue.

Source: Coding guidelines

Sources/StreamVideoSwiftUI/ViewFactory.swift (1)

293-304: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the obsolete iOS 14.0 availability check.

Since the deployment target has been raised to iOS 15, this availability check and its EmptyView fallback are no longer needed.

♻️ Proposed refactor
     public func makeParticipantsListView(
         viewModel: CallViewModel
     ) -> some View {
-        if `#available`(iOS 14.0, *) {
-            return CallParticipantsInfoView(
-                viewFactory: self,
-                callViewModel: viewModel
-            )
-        } else {
-            return EmptyView()
-        }
+        CallParticipantsInfoView(
+            viewFactory: self,
+            callViewModel: viewModel
+        )
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamVideoSwiftUI/ViewFactory.swift` around lines 293 - 304, Update
makeParticipantsListView to remove the iOS 14 availability branch and its
EmptyView fallback, returning CallParticipantsInfoView directly with the
existing viewFactory and callViewModel arguments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/StreamVideo/CallKit/CallKitAdapter.swift`:
- Around line 120-128: Update the CallKit configuration in the adapter
initialization flow so systems below iOS 27 always retain the CallKit service
when LiveCommunicationKit is unavailable. Adjust the assignment involving
callKitService.streamVideo and useLiveCommunicationKit to account for OS
availability, while preserving LiveCommunicationKit selection on iOS 27 and
later and the existing VideoConfig behavior where supported.
- Around line 44-46: The participantAutoLeavePolicy setter in CallKitAdapter
must not assign one ParticipantAutoLeavePolicy instance to both calling
services, because each service mutates its callback. Store or construct
independent policy instances per CallKitService and LiveCommunicationKitService,
and ensure the active backend receives the appropriate policy and callback when
switching services.

In `@Sources/StreamVideo/CallKit/LiveCommunicationKitService.swift`:
- Around line 643-650: Update the catch block in the call-accept flow around
callToJoinEntry.call.accept() to stop execution after acceptance fails. Before
returning, perform the required call, storage, and event cleanup, fail the
action via completeActionOnce, and ensure call.join() is not reached.
- Line 14: Remove the unsound shared-state access in LiveCommunicationKitService
before retaining `@unchecked` Sendable: confine active, cancellables, and mutable
CallEntry lifecycle state to one actor or serial executor, and route delegate
callbacks, tasks, timers, and muteProcessingQueue mutations through it. Extend
synchronization beyond _storage so all lifecycle reads and writes use the same
isolation boundary, preserving correct call routing and cancellation behavior.
- Around line 948-957: Ensure incoming conversations are reported only after a
managed call entry has been created in the affected flows around the shown call
lookup and the corresponding sections at 202-207 and 521-537. When StreamVideo
is unavailable or the CID is malformed or cannot resolve to a call, do not
report the conversation; instead end it directly by UUID if it was already
reported. Preserve storage via set(..., for:) only for valid managed calls so
later callEnded handling can always resolve the entry.
- Around line 489-493: Update setUpRingingTimer(for:) so autoCancelTimeoutMs is
converted to TimeInterval before dividing by 1000, preserving fractional-second
timeouts and avoiding truncation for values below 1,000 ms.

In `@Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift`:
- Around line 322-329: Update canBeReconnected() to select the calling service
using the same useLiveCommunicationKit gate as CallKitAdapter. On iOS 27+, read
liveCommunicationKitService.callCount only when that gate is enabled; otherwise
use callKitService.callCount, preserving the existing availability checks.

---

Outside diff comments:
In `@DemoApp/Sources/Components/AudioTrackPlayer/AudioTrackPlayer.swift`:
- Around line 31-46: Refactor the processingQueue task around AudioTrackPlayer
so Bundle URL lookup and AVAudioPlayer(contentsOf:) initialization occur off the
MainActor. After initialization completes, hop to MainActor for the existing
track validation, audioPlayer assignment, playback, loop count, and isPlaying
updates, preserving the weak-self and early-return behavior.

---

Nitpick comments:
In `@Sources/StreamVideoSwiftUI/ViewFactory.swift`:
- Around line 293-304: Update makeParticipantsListView to remove the iOS 14
availability branch and its EmptyView fallback, returning
CallParticipantsInfoView directly with the existing viewFactory and
callViewModel arguments.

In `@StreamVideoTests/VideoConfig_Tests.swift`:
- Around line 10-19: Rename the two VideoConfig test methods to remove “when”
while preserving their scenario and expected-result meaning:
test_init_useLiveCommunicationKitNotProvided_defaultsToTrue and
test_init_useLiveCommunicationKitFalse_setsValue.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0a5c629-3d77-4c1a-9da2-09d82f96df10

📥 Commits

Reviewing files that changed from the base of the PR and between d7426ef and 1fb3f16.

📒 Files selected for processing (36)
  • DemoApp/Sources/Components/AudioTrackPlayer/AudioTrackPlayer.swift
  • DemoApp/Sources/Components/Debug/Items/FeatureFlags/DebugMenu+FeatureFlagsView.swift
  • Sources/StreamVideo/CallKit/CallKitAdapter.swift
  • Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift
  • Sources/StreamVideo/CallKit/LiveCommunicationKitService.swift
  • Sources/StreamVideo/CallKit/SystemCallingService.swift
  • Sources/StreamVideo/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule.swift
  • Sources/StreamVideo/Utils/AudioSession/AudioDeviceModule/AudioEngineLevelNodeAdapter.swift
  • Sources/StreamVideo/Utils/AudioSession/AudioDeviceModule/Components/AudioBufferRenderer.swift
  • Sources/StreamVideo/VideoConfig.swift
  • Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift
  • Sources/StreamVideoSwiftUI/CallView/LocalParticipantViewModifier.swift
  • Sources/StreamVideoSwiftUI/CallView/PermissionsPromptView.swift
  • Sources/StreamVideoSwiftUI/CallView/SampleBufferVideoCallView.swift
  • Sources/StreamVideoSwiftUI/CallView/ViewModifiers/CallEndedViewModifier.swift
  • Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift
  • Sources/StreamVideoSwiftUI/CallingViews/iOS13/IncomingCallView_iOS13.swift
  • Sources/StreamVideoSwiftUI/CallingViews/iOS13/PreJoiningView_iOS13.swift
  • Sources/StreamVideoSwiftUI/CallingViews/iOS13/VideoView_iOS13.swift
  • Sources/StreamVideoSwiftUI/Utils/CallSoundsPlayer.swift
  • Sources/StreamVideoSwiftUI/ViewFactory.swift
  • StreamVideo.xcodeproj/project.pbxproj
  • StreamVideo.xcodeproj/xcshareddata/xcschemes/DemoApp.xcscheme
  • StreamVideo.xcodeproj/xcshareddata/xcschemes/StreamVideo.xcscheme
  • StreamVideo.xcodeproj/xcshareddata/xcschemes/StreamVideoSwiftUI.xcscheme
  • StreamVideoTests/CallKit/CallKitAdapterTests.swift
  • StreamVideoTests/CallKit/CallKitPushNotificationAdapterTests.swift
  • StreamVideoTests/CallKit/LiveCommunicationKitServiceTests.swift
  • StreamVideoTests/Mock/VideoConfig+Dummy.swift
  • StreamVideoTests/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule_Tests.swift
  • StreamVideoTests/Utils/AudioSession/AudioDeviceModule/AudioEngineLevelNodeAdapter_Tests.swift
  • StreamVideoTests/Utils/AudioSession/AudioDeviceModule/Components/AudioBufferRenderer_Tests.swift
  • StreamVideoTests/VideoConfig_Tests.swift
  • StreamVideoTests/WebRTC/SFU/SFUAdapter_Tests.swift
  • StreamVideoTests/WebRTC/SFU/SFUEventAdapter_Tests.swift
  • StreamVideoTests/WebRTC/v2/WebRTCStateAdapter_Tests.swift
💤 Files with no reviewable changes (5)
  • Sources/StreamVideoSwiftUI/CallingViews/iOS13/PreJoiningView_iOS13.swift
  • Sources/StreamVideoSwiftUI/CallingViews/iOS13/IncomingCallView_iOS13.swift
  • Sources/StreamVideoSwiftUI/CallingViews/iOS13/VideoView_iOS13.swift
  • Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift
  • Sources/StreamVideoSwiftUI/CallView/LocalParticipantViewModifier.swift

Comment on lines 44 to +46
open var participantAutoLeavePolicy: ParticipantAutoLeavePolicy {
get { callKitService.participantAutoLeavePolicy }
set { callKitService.participantAutoLeavePolicy = newValue }
get { activeSystemCallingService.participantAutoLeavePolicy }
set { updateSystemCallingServices { $0.participantAutoLeavePolicy = newValue } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C4 \
  'protocol ParticipantAutoLeavePolicy|class LastParticipantAutoLeavePolicy|struct LastParticipantAutoLeavePolicy|var participantAutoLeavePolicy|onPolicyTriggered' \
  Sources/StreamVideo

Repository: GetStream/stream-video-swift

Length of output: 13929


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## CallKitAdapter\n'
sed -n '1,220p' Sources/StreamVideo/CallKit/CallKitAdapter.swift

printf '\n## CallKitService policy handling\n'
sed -n '120,220p' Sources/StreamVideo/CallKit/CallKitService.swift

printf '\n## LiveCommunicationKitService policy handling\n'
sed -n '120,220p' Sources/StreamVideo/CallKit/LiveCommunicationKitService.swift

printf '\n## SystemCallingService protocol\n'
sed -n '1,120p' Sources/StreamVideo/CallKit/SystemCallingService.swift

Repository: GetStream/stream-video-swift

Length of output: 15778


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C6 'activeSystemCallingService|updateSystemCallingServices|LiveCommunicationKitService|CallKitService' Sources/StreamVideo/CallKit/CallKitAdapter.swift Sources/StreamVideo/CallKit/*.swift

Repository: GetStream/stream-video-swift

Length of output: 35716


Do not share one ParticipantAutoLeavePolicy instance across both services Sources/StreamVideo/CallKit/CallKitAdapter.swift:44-46 forwards the same policy to CallKitService and LiveCommunicationKitService, but each service rewrites onPolicyTriggered in didSet. The last assignment wins, so the other service loses its callback. Keep separate policy ownership or rebind the callback when switching backends.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamVideo/CallKit/CallKitAdapter.swift` around lines 44 - 46, The
participantAutoLeavePolicy setter in CallKitAdapter must not assign one
ParticipantAutoLeavePolicy instance to both calling services, because each
service mutates its callback. Store or construct independent policy instances
per CallKitService and LiveCommunicationKitService, and ensure the active
backend receives the appropriate policy and callback when switching services.

Comment on lines +120 to +128
let useLiveCommunicationKit = streamVideo.videoConfig.useLiveCommunicationKit
callKitService.streamVideo = useLiveCommunicationKit ? nil : streamVideo
#if canImport(LiveCommunicationKit)
if #available(iOS 27.0, *) {
InjectedValues[\.liveCommunicationKitService].streamVideo = useLiveCommunicationKit
? streamVideo
: nil
}
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Keep CallKit configured on systems without LiveCommunicationKit.

On iOS versions below 27, the default useLiveCommunicationKit == true sets callKitService.streamVideo to nil, while the availability block cannot configure the alternative service. This breaks system calling on every unsupported OS despite the VideoConfig contract.

Proposed fix
-        let useLiveCommunicationKit = streamVideo.videoConfig.useLiveCommunicationKit
-        callKitService.streamVideo = useLiveCommunicationKit ? nil : streamVideo
+        var useLiveCommunicationKit = false
         `#if` canImport(LiveCommunicationKit)
         if `#available`(iOS 27.0, *) {
+            useLiveCommunicationKit =
+                streamVideo.videoConfig.useLiveCommunicationKit
             InjectedValues[\.liveCommunicationKitService].streamVideo = useLiveCommunicationKit
                 ? streamVideo
                 : nil
         }
         `#endif`
+        callKitService.streamVideo = useLiveCommunicationKit ? nil : streamVideo
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let useLiveCommunicationKit = streamVideo.videoConfig.useLiveCommunicationKit
callKitService.streamVideo = useLiveCommunicationKit ? nil : streamVideo
#if canImport(LiveCommunicationKit)
if #available(iOS 27.0, *) {
InjectedValues[\.liveCommunicationKitService].streamVideo = useLiveCommunicationKit
? streamVideo
: nil
}
#endif
var useLiveCommunicationKit = false
`#if` canImport(LiveCommunicationKit)
if `#available`(iOS 27.0, *) {
useLiveCommunicationKit =
streamVideo.videoConfig.useLiveCommunicationKit
InjectedValues[\.liveCommunicationKitService].streamVideo = useLiveCommunicationKit
? streamVideo
: nil
}
`#endif`
callKitService.streamVideo = useLiveCommunicationKit ? nil : streamVideo
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamVideo/CallKit/CallKitAdapter.swift` around lines 120 - 128,
Update the CallKit configuration in the adapter initialization flow so systems
below iOS 27 always retain the CallKit service when LiveCommunicationKit is
unavailable. Adjust the assignment involving callKitService.streamVideo and
useLiveCommunicationKit to account for OS availability, while preserving
LiveCommunicationKit selection on iOS 27 and later and the existing VideoConfig
behavior where supported.


/// Manages LiveCommunicationKit integration for VoIP calls on iOS 27 and newer.
@available(iOS 27.0, *)
open class LiveCommunicationKitService: NSObject, ConversationManagerDelegate, SystemCallingService, @unchecked Sendable {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Isolate all mutable lifecycle state before claiming Sendable.

active, cancellables, and mutable CallEntry fields are accessed from delegate callbacks, tasks, timers, and muteProcessingQueue, while only _storage is locked. @unchecked Sendable therefore permits real data races and lifecycle misrouting. Confine the service to an actor/serial executor or synchronize all shared state consistently.

Also applies to: 53-60, 146-158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamVideo/CallKit/LiveCommunicationKitService.swift` at line 14,
Remove the unsound shared-state access in LiveCommunicationKitService before
retaining `@unchecked` Sendable: confine active, cancellables, and mutable
CallEntry lifecycle state to one actor or serial executor, and route delegate
callbacks, tasks, timers, and muteProcessingQueue mutations through it. Extend
synchronization beyond _storage so all lifecycle reads and writes use the same
isolation boundary, preserving correct call routing and cancellation behavior.

Comment on lines +489 to +493
open func setUpRingingTimer(for callState: GetCallResponse) {
let timeout = TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs / 1000)
ringingTimerCancellable = DefaultTimer
.publish(every: timeout)
.sink { [weak self] _ in

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve milliseconds when calculating the ringing timeout.

Line 490 performs integer division before conversion, truncating fractional seconds and producing zero for values below 1,000 ms.

Proposed fix
-        let timeout = TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs / 1000)
+        let timeout =
+            TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs) / 1_000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
open func setUpRingingTimer(for callState: GetCallResponse) {
let timeout = TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs / 1000)
ringingTimerCancellable = DefaultTimer
.publish(every: timeout)
.sink { [weak self] _ in
open func setUpRingingTimer(for callState: GetCallResponse) {
let timeout =
TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs) / 1_000
ringingTimerCancellable = DefaultTimer
.publish(every: timeout)
.sink { [weak self] _ in
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamVideo/CallKit/LiveCommunicationKitService.swift` around lines
489 - 493, Update setUpRingingTimer(for:) so autoCancelTimeoutMs is converted to
TimeInterval before dividing by 1000, preserving fractional-second timeouts and
avoiding truncation for values below 1,000 ms.

Comment on lines +643 to +650
do {
sendEvent(.accept)
reportConversationStartedConnecting(for: callToJoinEntry)
try await callToJoinEntry.call.accept()
} catch {
log.error(error, subsystems: .callKit)
completeActionOnce { action.fail() }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Stop the join flow when accepting the call fails.

After call.accept() throws, the code fails the system action but continues into call.join(). This can join a call after LiveCommunicationKit has already been told the action failed. Return after performing the appropriate call, storage, and event cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamVideo/CallKit/LiveCommunicationKitService.swift` around lines
643 - 650, Update the catch block in the call-accept flow around
callToJoinEntry.call.accept() to stop execution after acceptance fails. Before
returning, perform the required call, storage, and event cleanup, fail the
action via completeActionOnce, and ensure call.join() is not reached.

Comment on lines +948 to +957
let idComponents = cid.components(separatedBy: ":")
let uuid = uuidFactory.get()
if
idComponents.count >= 2,
let call = streamVideo?.call(
callType: idComponents[0],
callId: idComponents[1]
) {
set(.init(call: call, callUUID: uuid), for: uuid)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not report an incoming conversation without a managed call entry.

A missing StreamVideo or malformed CID prevents storage at Lines 948-957, but the conversation is still reported. The later callEnded call cannot find that entry, leaving the system conversation displayed indefinitely. Validate before reporting or end the conversation directly by UUID.

Also applies to: 202-207, 521-537

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamVideo/CallKit/LiveCommunicationKitService.swift` around lines
948 - 957, Ensure incoming conversations are reported only after a managed call
entry has been created in the affected flows around the shown call lookup and
the corresponding sections at 202-207 and 521-537. When StreamVideo is
unavailable or the CID is malformed or cannot resolve to a call, do not report
the conversation; instead end it directly by UUID if it was already reported.
Preserve storage via set(..., for:) only for valid managed calls so later
callEnded handling can always resolve the entry.

Comment on lines 322 to 329
func canBeReconnected() -> Bool {
callKitService.callCount > 0
#if canImport(LiveCommunicationKit)
if #available(iOS 27.0, *) {
return InjectedValues[\.liveCommunicationKitService].callCount > 0
}
#endif
return callKitService.callCount > 0
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate how the "active" system-calling service is resolved elsewhere,
# so ConnectionRecoveryHandler can reuse the same logic instead of an
# availability-only check.
rg -n -B2 -A10 'shouldUseLiveCommunicationKit|activeSystemCallingService|useLiveCommunicationKit' Sources/StreamVideo/CallKit

Repository: GetStream/stream-video-swift

Length of output: 12790


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- ConnectionRecoveryHandler.canBeReconnected ---'
ast-grep outline Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift --view expanded | sed -n '1,220p'

echo
echo '--- Relevant section ---'
sed -n '300,350p' Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift

echo
echo '--- CallKitAdapter service selection ---'
sed -n '88,130p' Sources/StreamVideo/CallKit/CallKitAdapter.swift

echo
echo '--- CallKitPushNotificationAdapter service selection ---'
sed -n '54,84p' Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift

Repository: GetStream/stream-video-swift

Length of output: 6068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- callCount definitions/usages ---'
rg -n -A4 -B4 '\bcallCount\b' Sources/StreamVideo/CallKit Sources/StreamVideo | sed -n '1,220p'

echo
echo '--- SystemCallingService declaration ---'
rg -n -A30 -B5 'protocol SystemCallingService|class .*SystemCallingService|struct .*SystemCallingService' Sources/StreamVideo/CallKit Sources/StreamVideo | sed -n '1,220p'

Repository: GetStream/stream-video-swift

Length of output: 20293


Use the active system-calling service here canBeReconnected() should follow the same useLiveCommunicationKit gate as CallKitAdapter; on iOS 27+ it currently reads liveCommunicationKitService.callCount even when CallKit is the configured path, which makes reconnection always fail in that setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift` around
lines 322 - 329, Update canBeReconnected() to select the calling service using
the same useLiveCommunicationKit gate as CallKitAdapter. On iOS 27+, read
liveCommunicationKitService.callCount only when that gate is enabled; otherwise
use callKitService.callCount, preserving the existing availability checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant