Implemented LiveCommunicationKit as an alternative to CallKit#1200
Implemented LiveCommunicationKit as an alternative to CallKit#1200martinmitrevski wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThis 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. ChangesLiveCommunicationKit calling
Audio playback and validation
iOS 15 SwiftUI platform cleanup
Test access robustness
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Generated by 🚫 Danger |
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) |
There was a problem hiding this comment.
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 winAvoid 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
MainActoronly 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 winAlign test names with the required convention.
Both method names include
when; rename them to omitgiven,when, andthenwhile 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, andthenfrom 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 valueRemove the obsolete
iOS 14.0availability check.Since the deployment target has been raised to iOS 15, this availability check and its
EmptyViewfallback 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
📒 Files selected for processing (36)
DemoApp/Sources/Components/AudioTrackPlayer/AudioTrackPlayer.swiftDemoApp/Sources/Components/Debug/Items/FeatureFlags/DebugMenu+FeatureFlagsView.swiftSources/StreamVideo/CallKit/CallKitAdapter.swiftSources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swiftSources/StreamVideo/CallKit/LiveCommunicationKitService.swiftSources/StreamVideo/CallKit/SystemCallingService.swiftSources/StreamVideo/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule.swiftSources/StreamVideo/Utils/AudioSession/AudioDeviceModule/AudioEngineLevelNodeAdapter.swiftSources/StreamVideo/Utils/AudioSession/AudioDeviceModule/Components/AudioBufferRenderer.swiftSources/StreamVideo/VideoConfig.swiftSources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swiftSources/StreamVideoSwiftUI/CallView/LocalParticipantViewModifier.swiftSources/StreamVideoSwiftUI/CallView/PermissionsPromptView.swiftSources/StreamVideoSwiftUI/CallView/SampleBufferVideoCallView.swiftSources/StreamVideoSwiftUI/CallView/ViewModifiers/CallEndedViewModifier.swiftSources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swiftSources/StreamVideoSwiftUI/CallingViews/iOS13/IncomingCallView_iOS13.swiftSources/StreamVideoSwiftUI/CallingViews/iOS13/PreJoiningView_iOS13.swiftSources/StreamVideoSwiftUI/CallingViews/iOS13/VideoView_iOS13.swiftSources/StreamVideoSwiftUI/Utils/CallSoundsPlayer.swiftSources/StreamVideoSwiftUI/ViewFactory.swiftStreamVideo.xcodeproj/project.pbxprojStreamVideo.xcodeproj/xcshareddata/xcschemes/DemoApp.xcschemeStreamVideo.xcodeproj/xcshareddata/xcschemes/StreamVideo.xcschemeStreamVideo.xcodeproj/xcshareddata/xcschemes/StreamVideoSwiftUI.xcschemeStreamVideoTests/CallKit/CallKitAdapterTests.swiftStreamVideoTests/CallKit/CallKitPushNotificationAdapterTests.swiftStreamVideoTests/CallKit/LiveCommunicationKitServiceTests.swiftStreamVideoTests/Mock/VideoConfig+Dummy.swiftStreamVideoTests/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule_Tests.swiftStreamVideoTests/Utils/AudioSession/AudioDeviceModule/AudioEngineLevelNodeAdapter_Tests.swiftStreamVideoTests/Utils/AudioSession/AudioDeviceModule/Components/AudioBufferRenderer_Tests.swiftStreamVideoTests/VideoConfig_Tests.swiftStreamVideoTests/WebRTC/SFU/SFUAdapter_Tests.swiftStreamVideoTests/WebRTC/SFU/SFUEventAdapter_Tests.swiftStreamVideoTests/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
| open var participantAutoLeavePolicy: ParticipantAutoLeavePolicy { | ||
| get { callKitService.participantAutoLeavePolicy } | ||
| set { callKitService.participantAutoLeavePolicy = newValue } | ||
| get { activeSystemCallingService.participantAutoLeavePolicy } | ||
| set { updateSystemCallingServices { $0.participantAutoLeavePolicy = newValue } } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C4 \
'protocol ParticipantAutoLeavePolicy|class LastParticipantAutoLeavePolicy|struct LastParticipantAutoLeavePolicy|var participantAutoLeavePolicy|onPolicyTriggered' \
Sources/StreamVideoRepository: 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.swiftRepository: 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/*.swiftRepository: 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.
| 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 |
There was a problem hiding this comment.
🎯 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.
| 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 { |
There was a problem hiding this comment.
🩺 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.
| open func setUpRingingTimer(for callState: GetCallResponse) { | ||
| let timeout = TimeInterval(callState.call.settings.ring.autoCancelTimeoutMs / 1000) | ||
| ringingTimerCancellable = DefaultTimer | ||
| .publish(every: timeout) | ||
| .sink { [weak self] _ in |
There was a problem hiding this comment.
🎯 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.
| 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.
| do { | ||
| sendEvent(.accept) | ||
| reportConversationStartedConnecting(for: callToJoinEntry) | ||
| try await callToJoinEntry.call.accept() | ||
| } catch { | ||
| log.error(error, subsystems: .callKit) | ||
| completeActionOnce { action.fail() } | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🩺 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.
| func canBeReconnected() -> Bool { | ||
| callKitService.callCount > 0 | ||
| #if canImport(LiveCommunicationKit) | ||
| if #available(iOS 27.0, *) { | ||
| return InjectedValues[\.liveCommunicationKitService].callCount > 0 | ||
| } | ||
| #endif | ||
| return callKitService.callCount > 0 | ||
| } |
There was a problem hiding this comment.
🩺 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/CallKitRepository: 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.swiftRepository: 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.
🔗 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.
imgimg🧪 Manual Testing Notes
Explain how this change can be tested manually, if applicable.
☑️ Contributor Checklist
🎁 Meme
Provide a funny gif or image that relates to your work on this pull request. (Optional)
Summary by CodeRabbit
New Features
Bug Fixes
Compatibility