Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import AVFoundation
import Foundation
import StreamVideo

final class AudioTrackPlayer: NSObject, AVAudioPlayerDelegate, @unchecked Sendable {
final class AudioTrackPlayer: NSObject, @unchecked Sendable {
enum Track: String, Equatable, CaseIterable {
case track1 = "track_1"
case track2 = "track_2"
Expand Down Expand Up @@ -60,15 +60,6 @@ final class AudioTrackPlayer: NSObject, AVAudioPlayerDelegate, @unchecked Sendab
track = nil
}
}

// MARK: - AVAudioPlayerDelegate

func audioPlayerDidFinishPlaying(
_ player: AVAudioPlayer,
successfully flag: Bool
) {
stop()
}
}

extension AudioTrackPlayer: InjectionKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension AppEnvironment {
var viewProvider: () -> AnyView
}

nonisolated(unsafe) static var featureFlags: [FeatureFlag] = [
@MainActor static var featureFlags: [FeatureFlag] = [
.init { .init(DebugMenu.VideoProcessingPipelineToggleView()) },
.init { .init(DebugMenu.CapturingPipelineToggleView()) },
.init { .init(DebugMenu.VideoRenderingMenuView()) },
Expand Down
73 changes: 57 additions & 16 deletions Sources/StreamVideo/CallKit/CallKitAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,36 @@ open class CallKitAdapter {

/// The icon data used as the template for CallKit.
open var iconTemplateImageData: Data? {
get { callKitService.iconTemplateImageData }
set { callKitService.iconTemplateImageData = newValue }
get { activeSystemCallingService.iconTemplateImageData }
set { updateSystemCallingServices { $0.iconTemplateImageData = newValue } }
}

/// The ringtone sound to use for CallKit ringing calls.
open var ringtoneSound: String? {
get { callKitService.ringtoneSound }
set { callKitService.ringtoneSound = newValue }
get { activeSystemCallingService.ringtoneSound }
set { updateSystemCallingServices { $0.ringtoneSound = newValue } }
}

/// Configure whether calls should appear in the Recents app.
open var includesCallsInRecents: Bool {
get { callKitService.includesCallsInRecents }
set { callKitService.includesCallsInRecents = newValue }
get { activeSystemCallingService.includesCallsInRecents }
set { updateSystemCallingServices { $0.includesCallsInRecents = newValue } }
}

/// The callSettings to use when joining a call (after accepting it on CallKit)
/// default: nil
/// The callSettings to use when joining a call after accepting it on the
/// system calling UI. Default: `nil`.
open var callSettings: CallSettings? {
didSet { callKitService.callSettings = callSettings }
didSet { updateSystemCallingServices { $0.callSettings = callSettings } }
}

/// The policy that decides if a CallKit-managed call should leave
/// The policy that decides if a system-managed call should leave
/// automatically when participant state changes.
open var participantAutoLeavePolicy: ParticipantAutoLeavePolicy {
get { callKitService.participantAutoLeavePolicy }
set { callKitService.participantAutoLeavePolicy = newValue }
get { activeSystemCallingService.participantAutoLeavePolicy }
set { updateSystemCallingServices { $0.participantAutoLeavePolicy = newValue } }
Comment on lines 44 to +46

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.

}

/// The policy defining the availability of CallKit services.
/// The policy defining the availability of system calling services.
///
/// - Default: `.regionBased`
public var availabilityPolicy: CallKitAvailabilityPolicy = .regionBased
Expand All @@ -64,8 +64,8 @@ open class CallKitAdapter {
/// join flow, such as waiting for another participant or validating an
/// external precondition. Throw from the interceptor to fail the join.
public var callJoinInterceptor: CallJoinIntercepting? {
get { callKitService.callJoinInterceptor }
set { callKitService.callJoinInterceptor = newValue }
get { activeSystemCallingService.callJoinInterceptor }
set { updateSystemCallingServices { $0.callJoinInterceptor = newValue } }
}

/// Initializes the `CallKitAdapter`.
Expand All @@ -87,6 +87,47 @@ open class CallKitAdapter {
callKitPushNotificationAdapter.unregister()
}

private var activeSystemCallingService: SystemCallingService {
#if canImport(LiveCommunicationKit)
if #available(iOS 27.0, *), shouldUseLiveCommunicationKit {
return InjectedValues[\.liveCommunicationKitService]
}
#endif
return callKitService
}

private var shouldUseLiveCommunicationKit: Bool {
streamVideo?.videoConfig.useLiveCommunicationKit ?? true
}

private func updateSystemCallingServices(
_ update: (SystemCallingService) -> Void
) {
update(callKitService)
#if canImport(LiveCommunicationKit)
if #available(iOS 27.0, *) {
update(InjectedValues[\.liveCommunicationKitService])
}
#endif
}

private func updateSystemCallingServices(with streamVideo: StreamVideo?) {
guard let streamVideo else {
updateSystemCallingServices { $0.streamVideo = nil }
return
}

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
Comment on lines +120 to +128

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.

}

private func didUpdate(_ streamVideo: StreamVideo?) {
guard availabilityPolicy.policy.isAvailable else {
log
Expand All @@ -96,7 +137,7 @@ open class CallKitAdapter {
return
}

callKitService.streamVideo = streamVideo
updateSystemCallingServices(with: streamVideo)

guard streamVideo != nil else {
unregisterForIncomingCalls()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,43 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs

@Injected(\.callKitService) private var callKitService

private final class SendableCompletion: @unchecked Sendable {
private let completion: () -> Void

init(_ completion: @escaping () -> Void) {
self.completion = completion
}

func callAsFunction() {
completion()
}
}

private var activeSystemCallingService: SystemCallingService {
#if canImport(LiveCommunicationKit)
if #available(iOS 27.0, *), shouldUseLiveCommunicationKit {
return InjectedValues[\.liveCommunicationKitService]
}
#endif
return callKitService
}

private var configuredStreamVideo: StreamVideo? {
if let streamVideo = callKitService.streamVideo {
return streamVideo
}
#if canImport(LiveCommunicationKit)
if #available(iOS 27.0, *) {
return InjectedValues[\.liveCommunicationKitService].streamVideo
}
#endif
return nil
}

private var shouldUseLiveCommunicationKit: Bool {
configuredStreamVideo?.videoConfig.useLiveCommunicationKit ?? true
}

/// The push registry used for VoIP push notifications.
open private(set) lazy var registry: PKPushRegistry = .init(queue: .init(label: "io.getstream.voip"))

Expand Down Expand Up @@ -106,8 +143,11 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs
for type: PKPushType,
completion: @escaping () -> Void
) {
defer { completion() }
guard type == .voIP else { return }
let pushCompletion = SendableCompletion(completion)
guard type == .voIP else {
pushCompletion()
return
}

let content = decodePayload(payload)

Expand All @@ -116,7 +156,7 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs
"Received VoIP push notification with cid:\(content.cid) callerId:\(content.callerId) callerName:\(content.localizedCallerName)."
)

callKitService.reportIncomingCall(
activeSystemCallingService.reportIncomingCall(
content.cid,
localizedCallerName: content.localizedCallerName,
callerId: content.callerId,
Expand All @@ -125,6 +165,7 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs
if let error {
log.error(error, subsystems: .callKit)
}
pushCompletion()
}
)
}
Expand Down
Loading
Loading