diff --git a/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift b/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift index 92685bf59..3a4a2fd99 100644 --- a/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift +++ b/DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/03-callkit-integration.swift @@ -160,7 +160,12 @@ fileprivate func content() { callType: .default, callId: UUID().uuidString, members: [.init(userId: name)], - ring: true + ring: true, + + // hasVideo: A boolean indicating if the call + // will be video or only audio. Still requires + // appropriate setting of ``CallSettings`.` + video: true ) } } diff --git a/Sources/StreamVideo/Call.swift b/Sources/StreamVideo/Call.swift index 9edc989b2..a674acf32 100644 --- a/Sources/StreamVideo/Call.swift +++ b/Sources/StreamVideo/Call.swift @@ -225,6 +225,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { /// - maxDuration: An optional integer representing the maximum duration of the call in seconds. /// - maxParticipants: An optional integer representing the maximum number of participants allowed in the call. /// - backstage: An optional backstage request. + /// - video: A boolean indicating if the call will be video or only audio. Still requires appropriate + /// setting of ``CallSettings`.` /// - Returns: A `CallResponse` object representing the created call. /// - Throws: An error if the call creation fails. @discardableResult @@ -238,7 +240,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { notify: Bool = false, maxDuration: Int? = nil, maxParticipants: Int? = nil, - backstage: BackstageSettingsRequest? = nil + backstage: BackstageSettingsRequest? = nil, + video: Bool? = nil ) async throws -> CallResponse { var membersRequest = [MemberRequest]() memberIds?.forEach { @@ -268,7 +271,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { members: membersRequest, settingsOverride: settingsOverride, startsAt: startsAt, - team: team + team: team, + video: video ), notify: notify, ring: ring diff --git a/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift b/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift index 86d1f8b38..0cb2301a0 100644 --- a/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift +++ b/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift @@ -15,6 +15,7 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs case displayName = "call_display_name" case createdByName = "created_by_display_name" case createdById = "created_by_id" + case video } /// Represents the content of a VoIP push notification. @@ -22,15 +23,18 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs var cid: String var localizedCallerName: String var callerId: String + var hasVideo: Bool public init( cid: String, localizedCallerName: String, - callerId: String + callerId: String, + hasVideo: Bool ) { self.cid = cid self.localizedCallerName = localizedCallerName self.callerId = callerId + self.hasVideo = hasVideo } } @@ -107,6 +111,7 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs content.cid, localizedCallerName: content.localizedCallerName, callerId: content.callerId, + hasVideo: content.hasVideo, completion: { error in if let error { log.error(error) @@ -130,7 +135,8 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs return .init( cid: "unknown", localizedCallerName: defaultCallText, - callerId: defaultCallText + callerId: defaultCallText, + hasVideo: false ) } @@ -148,10 +154,21 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs fallback: defaultCallText ) + let hasVideo: Bool = { + if let booleanValue = streamDict[PayloadKey.video.rawValue] as? Bool { + return booleanValue + } else if let stringValue = streamDict[PayloadKey.video.rawValue] as? String { + return stringValue == "true" + } else { + return false + } + }() + return .init( cid: cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: hasVideo ) } } diff --git a/Sources/StreamVideo/CallKit/CallKitService.swift b/Sources/StreamVideo/CallKit/CallKitService.swift index 507462912..7ed6ab1ad 100644 --- a/Sources/StreamVideo/CallKit/CallKitService.swift +++ b/Sources/StreamVideo/CallKit/CallKitService.swift @@ -109,18 +109,21 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { /// - cid: The call ID. /// - localizedCallerName: The localized caller name. /// - callerId: The caller's identifier. + /// - hasVideo: Indicator if call is video or audio. /// - completion: A closure to be called upon completion. @MainActor open func reportIncomingCall( _ cid: String, localizedCallerName: String, callerId: String, + hasVideo: Bool = false, completion: @escaping (Error?) -> Void ) { let (callUUID, callUpdate) = buildCallUpdate( cid: cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: hasVideo ) callProvider.reportNewIncomingCall( @@ -136,6 +139,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { cid:\(cid) callerId:\(callerId) callerName:\(localizedCallerName) + hasVideo: \(hasVideo) """ ) @@ -573,7 +577,8 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { private func buildCallUpdate( cid: String, localizedCallerName: String, - callerId: String + callerId: String, + hasVideo: Bool ) -> (UUID, CXCallUpdate) { let update = CXCallUpdate() let idComponents = cid.components(separatedBy: ":") @@ -589,7 +594,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { update.localizedCallerName = localizedCallerName update.remoteHandle = CXHandle(type: .generic, value: callerId) - update.hasVideo = supportsVideo + update.hasVideo = hasVideo update.supportsDTMF = false if supportsHolding { diff --git a/Sources/StreamVideoSwiftUI/CallViewModel.swift b/Sources/StreamVideoSwiftUI/CallViewModel.swift index 2996a2977..b65300056 100644 --- a/Sources/StreamVideoSwiftUI/CallViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallViewModel.swift @@ -328,6 +328,8 @@ open class CallViewModel: ObservableObject { /// - maxParticipants: An optional integer representing the maximum number of participants allowed in the call. /// - startsAt: An optional date when the call starts. /// - backstage: An optional request for setting up backstage. + /// - video: A boolean indicating if the call will be video or only audio. Still requires appropriate + /// setting of ``CallSettings`.` public func startCall( callType: String, callId: String, @@ -337,7 +339,8 @@ open class CallViewModel: ObservableObject { maxParticipants: Int? = nil, startsAt: Date? = nil, backstage: BackstageSettingsRequest? = nil, - customData: [String: RawJSON]? = nil + customData: [String: RawJSON]? = nil, + video: Bool? = nil ) { outgoingCallMembers = members callingState = ring ? .outgoing : .joining @@ -368,7 +371,8 @@ open class CallViewModel: ObservableObject { custom: customData, ring: ring, maxDuration: maxDuration, - maxParticipants: maxParticipants + maxParticipants: maxParticipants, + video: video ) let timeoutSeconds = TimeInterval( callData.settings.ring.autoCancelTimeoutMs / 1000 diff --git a/Sources/StreamVideoSwiftUI/Models/CallEventsHandler.swift b/Sources/StreamVideoSwiftUI/Models/CallEventsHandler.swift index b66ca5d11..48f1d80e6 100644 --- a/Sources/StreamVideoSwiftUI/Models/CallEventsHandler.swift +++ b/Sources/StreamVideoSwiftUI/Models/CallEventsHandler.swift @@ -55,7 +55,8 @@ public class CallEventsHandler { caller: caller, type: type, members: members, - timeout: TimeInterval(ringEvent.call.settings.ring.autoCancelTimeoutMs / 1000) + timeout: TimeInterval(ringEvent.call.settings.ring.autoCancelTimeoutMs / 1000), + video: ringEvent.video ) return .incoming(incomingCall) case let .typeCallSessionStartedEvent(callSessionStartedEvent): diff --git a/Sources/StreamVideoSwiftUI/Models/IncomingCall.swift b/Sources/StreamVideoSwiftUI/Models/IncomingCall.swift index 081c72ca7..6229bc90b 100644 --- a/Sources/StreamVideoSwiftUI/Models/IncomingCall.swift +++ b/Sources/StreamVideoSwiftUI/Models/IncomingCall.swift @@ -17,18 +17,21 @@ public struct IncomingCall: Identifiable, Sendable, Equatable { public let type: String public let members: [Member] public let timeout: TimeInterval + public let video: Bool public init( id: String, caller: User, type: String, members: [Member], - timeout: TimeInterval + timeout: TimeInterval, + video: Bool = false ) { self.id = id self.caller = caller self.type = type self.members = members self.timeout = timeout + self.video = video } } diff --git a/StreamVideoSwiftUITests/CallingViews/IncomingCallView_Tests.swift b/StreamVideoSwiftUITests/CallingViews/IncomingCallView_Tests.swift index 5eb492100..8b0d64987 100644 --- a/StreamVideoSwiftUITests/CallingViews/IncomingCallView_Tests.swift +++ b/StreamVideoSwiftUITests/CallingViews/IncomingCallView_Tests.swift @@ -19,7 +19,8 @@ final class IncomingCallView_Tests: StreamVideoUITestCase { caller: members.first!.user, type: callType, members: members, - timeout: 15000 + timeout: 15000, + video: false ) let view = IncomingCallView( callInfo: callInfo, diff --git a/StreamVideoTests/CallKit/CallKitPushNotificationAdapterTests.swift b/StreamVideoTests/CallKit/CallKitPushNotificationAdapterTests.swift index 9b9571885..7db3c8cb0 100644 --- a/StreamVideoTests/CallKit/CallKitPushNotificationAdapterTests.swift +++ b/StreamVideoTests/CallKit/CallKitPushNotificationAdapterTests.swift @@ -98,7 +98,8 @@ final class CallKitPushNotificationAdapterTests: XCTestCase { .init( cid: "123", localizedCallerName: "TestUser", - callerId: "test_user" + callerId: "test_user", + hasVideo: false ) ) } @@ -109,7 +110,8 @@ final class CallKitPushNotificationAdapterTests: XCTestCase { .init( cid: "123", localizedCallerName: "TestUser", - callerId: "test_user" + callerId: "test_user", + hasVideo: false ), displayName: "Stream Group Call" ) @@ -132,7 +134,7 @@ final class CallKitPushNotificationAdapterTests: XCTestCase { ) { let pushPayload = MockPKPushPayload() pushPayload.stubType = contentType - pushPayload.stubDictionaryPayload = content.map { [ + var payload: [String: Any] = content.map { [ "stream": [ "call_cid": $0.cid, "call_display_name": displayName, @@ -141,6 +143,13 @@ final class CallKitPushNotificationAdapterTests: XCTestCase { ] ] } ?? [:] + if let hasVideo = content?.hasVideo, var streamPayload = payload["stream"] as? [String: Any] { + streamPayload["video"] = hasVideo + payload["stream"] = streamPayload + } + + pushPayload.stubDictionaryPayload = payload + let completionWasCalledExpectation = expectation(description: "Completion was called.") completionWasCalledExpectation.isInverted = content == nil subject.pushRegistry( @@ -169,6 +178,12 @@ final class CallKitPushNotificationAdapterTests: XCTestCase { file: file, line: line ) + XCTAssertEqual( + callKitService.reportIncomingCallWasCalled?.hasVideo, + content.hasVideo, + file: file, + line: line + ) callKitService.reportIncomingCallWasCalled?.completion(nil) } else { XCTAssertNil( diff --git a/StreamVideoTests/CallKit/CallKitServiceTests.swift b/StreamVideoTests/CallKit/CallKitServiceTests.swift index 8f68ce4a4..f8b5b5ae6 100644 --- a/StreamVideoTests/CallKit/CallKitServiceTests.swift +++ b/StreamVideoTests/CallKit/CallKitServiceTests.swift @@ -57,13 +57,12 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { // MARK: - reportIncomingCall @MainActor - func test_reportIncomingCall_supportsVideo_callUpdateWasConfiguredCorrectly() throws { - subject.supportsVideo = true - + func test_reportIncomingCall_hasVideoTrue_callUpdateWasConfiguredCorrectly() throws { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: true ) { _ in } let invocation = try XCTUnwrap(callProvider.invocations.first) @@ -77,13 +76,12 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { } @MainActor - func test_reportIncomingCall_doesNotSupportVideo_callUpdateWasConfiguredCorrectly() throws { - subject.supportsVideo = false - + func test_reportIncomingCall_hasVideoFalse_callUpdateWasConfiguredCorrectly() throws { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } let invocation = try XCTUnwrap(callProvider.invocations.first) @@ -105,7 +103,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } XCTAssertEqual(subject.callProvider.configuration.iconTemplateImageData, expectedData) @@ -118,7 +117,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } XCTAssertNil(subject.callProvider.configuration.iconTemplateImageData) @@ -134,7 +134,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { error in completionError = error expectation.fulfill() @@ -157,7 +158,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } } } @@ -174,7 +176,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } } } @@ -236,7 +239,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } let waitExpectation = self.expectation(description: "Wait expectation") @@ -267,7 +271,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } } } @@ -288,7 +293,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } let waitExpectationA = self.expectation(description: "a") @@ -299,7 +305,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } } } @@ -319,7 +326,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } await waitExpectation(timeout: 1) @@ -352,7 +360,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } await waitExpectation(timeout: 1) @@ -385,7 +394,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } await waitExpectation(timeout: 1) // Accept call @@ -421,7 +431,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } await assertReportCallEnded(.answeredElsewhere) { @@ -448,7 +459,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } await assertReportCallEnded(.declinedElsewhere) { @@ -472,7 +484,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } subject.provider( @@ -502,7 +515,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( secondCallId, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } XCTAssertEqual(subject.callCount, 2) @@ -529,7 +543,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } try await assertRequestTransaction(CXEndCallAction.self) { @@ -549,7 +564,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } await waitExpectation(timeout: 2) @@ -587,7 +603,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } await waitExpectation(timeout: 2) @@ -746,7 +763,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.reportIncomingCall( cid, localizedCallerName: localizedCallerName, - callerId: callerId + callerId: callerId, + hasVideo: false ) { _ in } } } diff --git a/StreamVideoTests/Utilities/Mocks/MockCallKitService.swift b/StreamVideoTests/Utilities/Mocks/MockCallKitService.swift index b27afe44c..9fe204317 100644 --- a/StreamVideoTests/Utilities/Mocks/MockCallKitService.swift +++ b/StreamVideoTests/Utilities/Mocks/MockCallKitService.swift @@ -6,7 +6,13 @@ import Foundation import StreamVideo final class MockCallKitService: CallKitService { - private(set) var reportIncomingCallWasCalled: (cid: String, callerName: String, callerId: String, completion: (Error?) -> Void)? + private(set) var reportIncomingCallWasCalled: ( + cid: String, + callerName: String, + callerId: String, + hasVideo: Bool?, + completion: (Error?) -> Void + )? override init() { super.init() } @@ -14,8 +20,9 @@ final class MockCallKitService: CallKitService { _ cid: String, localizedCallerName: String, callerId: String, + hasVideo: Bool?, completion: @escaping ((any Error)?) -> Void ) { - reportIncomingCallWasCalled = (cid, localizedCallerName, callerId, completion) + reportIncomingCallWasCalled = (cid, localizedCallerName, callerId, hasVideo, completion) } }