diff --git a/.github/workflows/release-start.yml b/.github/workflows/release-start.yml index b96e7f0d0..3dcd173e5 100644 --- a/.github/workflows/release-start.yml +++ b/.github/workflows/release-start.yml @@ -26,6 +26,11 @@ jobs: - uses: ./.github/actions/xcode-cache + - name: Integration Test + run: gh workflow run test.yml --repo GetStream/apple-internal-testing-pipeline # reports to @support on failure + env: + GITHUB_TOKEN: ${{ secrets.CI_BOT_GITHUB_TOKEN }} + - name: Create Release PR run: bundle exec fastlane release version:"${{ github.event.inputs.version }}" --verbose env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dfb24eff..6ca43a60e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🔄 Changed +# [1.38.2](https://github.com/GetStream/stream-video-swift/releases/tag/1.38.2) +_December 22, 2025_ + +### 🔄 Changed +- Improve reconnection logic. [#1013](https://github.com/GetStream/stream-video-swift/pull/1013) + # [1.38.1](https://github.com/GetStream/stream-video-swift/releases/tag/1.38.1) _December 15, 2025_ diff --git a/Gemfile.lock b/Gemfile.lock index d7434a2ab..a739aa590 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,8 +26,8 @@ GEM ast (2.4.3) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1139.0) - aws-sdk-core (3.228.0) + aws-partitions (1.1197.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -35,11 +35,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.109.0) - aws-sdk-core (~> 3, >= 3.228.0) + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.195.0) - aws-sdk-core (~> 3, >= 3.228.0) + aws-sdk-s3 (1.208.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -51,7 +51,7 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) base64 (0.3.0) benchmark (0.4.1) - bigdecimal (3.2.2) + bigdecimal (4.0.1) claide (1.1.0) claide-plugins (0.9.2) cork @@ -211,7 +211,7 @@ GEM bundler fastlane pry - fastlane-plugin-stream_actions (0.3.101) + fastlane-plugin-stream_actions (0.3.102) xctest_list (= 1.2.1) fastlane-plugin-versioning (0.7.1) fastlane-plugin-xcsize (1.2.0) @@ -433,7 +433,7 @@ DEPENDENCIES fastlane fastlane-plugin-create_xcframework fastlane-plugin-lizard - fastlane-plugin-stream_actions (= 0.3.101) + fastlane-plugin-stream_actions (= 0.3.102) fastlane-plugin-versioning fastlane-plugin-xcsize (= 1.2.0) json diff --git a/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift b/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift index 3f977d45e..a6a25e305 100644 --- a/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift +++ b/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift @@ -7,7 +7,7 @@ import Foundation extension SystemEnvironment { /// A Stream Video version. - public static let version: String = "1.38.1" + public static let version: String = "1.38.2" /// The WebRTC version. public static let webRTCVersion: String = "137.0.54" } diff --git a/Sources/StreamVideo/HTTPClient/InternetConnection.swift b/Sources/StreamVideo/HTTPClient/InternetConnection.swift index d0bedd174..292a843dd 100644 --- a/Sources/StreamVideo/HTTPClient/InternetConnection.swift +++ b/Sources/StreamVideo/HTTPClient/InternetConnection.swift @@ -30,10 +30,6 @@ final class InternetConnection: @unchecked Sendable { /// The current Internet connection status. @Published private(set) var status: InternetConnectionStatus { didSet { - guard oldValue != status else { return } - - log.info("Internet Connection: \(status)", subsystems: .httpRequests) - postNotification(.internetConnectionStatusDidChange, with: status) guard oldValue.isAvailable != status.isAvailable else { return } @@ -42,6 +38,9 @@ final class InternetConnection: @unchecked Sendable { } } + private let subject: PassthroughSubject = .init() + private var processingCancellable: AnyCancellable? + /// The notification center that posts notifications when connection state changes.. let notificationCenter: NotificationCenter @@ -56,8 +55,14 @@ final class InternetConnection: @unchecked Sendable { ) { self.notificationCenter = notificationCenter self.monitor = monitor + self.status = monitor.status + + processingCancellable = subject + .removeDuplicates() + .log(.debug) { "Internet Connection: \($0)" } + .receive(on: DispatchQueue.main) + .assign(to: \.status, onWeak: self) - status = monitor.status monitor.delegate = self monitor.start() } @@ -69,7 +74,7 @@ final class InternetConnection: @unchecked Sendable { extension InternetConnection: InternetConnectionDelegate { func internetConnectionStatusDidChange(status: InternetConnectionStatus) { - self.status = status + subject.send(status) } } @@ -211,6 +216,8 @@ extension InternetConnection { /// A protocol defining the interface for internet connection monitoring. public protocol InternetConnectionProtocol { + var status: InternetConnectionStatus { get } + /// A publisher that emits the current internet connection status. /// /// This publisher never fails and continuously updates with the latest @@ -226,7 +233,10 @@ extension InternetConnection: InternetConnectionProtocol { /// /// - Note: The publisher won't publish any duplicates. public var statusPublisher: AnyPublisher { - $status.removeDuplicates().eraseToAnyPublisher() + $status + .debounce(for: .seconds(0.1), scheduler: DispatchQueue.main) + .removeDuplicates() + .eraseToAnyPublisher() } } diff --git a/Sources/StreamVideo/Info.plist b/Sources/StreamVideo/Info.plist index a8ea0f1ec..ec8879a18 100644 --- a/Sources/StreamVideo/Info.plist +++ b/Sources/StreamVideo/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.38.1 + 1.38.2 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Components/RTCAudioSessionPublisher.swift b/Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Components/RTCAudioSessionPublisher.swift index 3a50a5c9d..196f8c18a 100644 --- a/Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Components/RTCAudioSessionPublisher.swift +++ b/Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Components/RTCAudioSessionPublisher.swift @@ -21,6 +21,24 @@ final class RTCAudioSessionPublisher: NSObject, RTCAudioSessionDelegate, @unchec from: AVAudioSessionRouteDescription, to: AVAudioSessionRouteDescription ) + + static func == (lhs: Event, rhs: Event) -> Bool { + switch (lhs, rhs) { + case (.didBeginInterruption, .didBeginInterruption): + return true + + case (let .didEndInterruption(lhsValue), let .didEndInterruption(rhsValue)): + return lhsValue == rhsValue + + case (let .didChangeRoute(lReason, lFrom, lTo), let .didChangeRoute(rReason, rFrom, rTo)): + return lReason == rReason + && RTCAudioStore.StoreState.AudioRoute(lFrom) == RTCAudioStore.StoreState.AudioRoute(rFrom) + && RTCAudioStore.StoreState.AudioRoute(lTo) == RTCAudioStore.StoreState.AudioRoute(rTo) + + default: + return false + } + } } /// The Combine publisher that emits session events. diff --git a/Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Namespace/RTCAudioStore+State.swift b/Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Namespace/RTCAudioStore+State.swift index a90c8b201..b156a7e94 100644 --- a/Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Namespace/RTCAudioStore+State.swift +++ b/Sources/StreamVideo/Utils/AudioSession/RTCAudioStore/Namespace/RTCAudioStore+State.swift @@ -164,6 +164,16 @@ extension RTCAudioStore { self.channels = channels self.source = nil } + + static func == (lhs: Port, rhs: Port) -> Bool { + lhs.type == rhs.type + && lhs.name == rhs.name + && lhs.id == rhs.id + && lhs.isExternal == rhs.isExternal + && lhs.isSpeaker == rhs.isSpeaker + && lhs.isReceiver == rhs.isReceiver + && lhs.channels == rhs.channels + } } let inputs: [Port] diff --git a/Sources/StreamVideo/Utils/ReflectiveStringConvertible/ReflectiveStringConvertible.swift b/Sources/StreamVideo/Utils/ReflectiveStringConvertible/ReflectiveStringConvertible.swift index fbcf12d29..72c88d43a 100644 --- a/Sources/StreamVideo/Utils/ReflectiveStringConvertible/ReflectiveStringConvertible.swift +++ b/Sources/StreamVideo/Utils/ReflectiveStringConvertible/ReflectiveStringConvertible.swift @@ -127,7 +127,7 @@ public extension ReflectiveStringConvertible { /// The default separator used to join different parts of the string representation. /// /// By default, this is set to a newline character ("\n"). - var separator: String { "\n" } + var separator: String { ", " } /// The default set of properties to be excluded from the string representation. /// @@ -169,11 +169,16 @@ public extension ReflectiveStringConvertible { /// /// - Returns: A string representation of the object. var description: String { + #if STREAM_TESTS + // During tests we allow full error logging. + #else guard LogConfig.level == .debug else { return "\(type(of: self))" } + #endif let mirror = Mirror(reflecting: self) - var output: [String] = ["Type: \(type(of: self))"] + var result = "\(type(of: self))" + var components: [String] = [] let excludedProperties = self.excludedProperties mirror @@ -188,9 +193,14 @@ public extension ReflectiveStringConvertible { } } .forEach { (child: (label: String, value: Any)) in - output.append(" - \(child.label): \(child.value)") + components.append("\(child.label):\(child.value)") } - return output.joined(separator: separator) + if !components.isEmpty { + result += " { " + result += components.joined(separator: separator) + result += " }" + } + return result } } diff --git a/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Disconnected.swift b/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Disconnected.swift index 530fb3a3f..3e2d9a8e7 100644 --- a/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Disconnected.swift +++ b/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Disconnected.swift @@ -33,6 +33,7 @@ extension WebRTCCoordinator.StateMachine.Stage { private var internetObservationCancellable: AnyCancellable? private var timeInStageCancellable: AnyCancellable? private var disposableBag = DisposableBag() + private let processingQueue = OperationQueue(maxConcurrentOperationCount: 1) /// Initializes a new instance of `DisconnectedStage`. /// - Parameter context: The context for the disconnected stage. @@ -180,10 +181,11 @@ extension WebRTCCoordinator.StateMachine.Stage { internetObservationCancellable?.cancel() internetObservationCancellable = internetConnectionObserver .statusPublisher - .receive(on: DispatchQueue.main) .filter { $0 != .unknown } .log(.debug, subsystems: .webRTC) { "Internet connection status updated to \($0)" } + .debounce(for: 1, scheduler: processingQueue) .removeDuplicates() + .receive(on: processingQueue) .sinkTask(storeIn: disposableBag) { [weak self] in /// Trace internet connection changes await self? @@ -223,6 +225,7 @@ extension WebRTCCoordinator.StateMachine.Stage { } timeInStageCancellable = DefaultTimer .publish(every: context.disconnectionTimeout) + .receive(on: processingQueue) .sink { [weak self] _ in self?.didTimeInStageExpired() } } diff --git a/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift b/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift index 633489ca2..31e169755 100644 --- a/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift +++ b/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift @@ -32,6 +32,7 @@ extension WebRTCCoordinator.StateMachine.Stage { private let disposableBag = DisposableBag() private var updateSubscriptionsAdapter: WebRTCUpdateSubscriptionsAdapter? + private let processingQueue = OperationQueue(maxConcurrentOperationCount: 1) /// Initializes a new instance of `JoinedStage`. /// - Parameter context: The context for the joined stage. @@ -209,6 +210,7 @@ extension WebRTCCoordinator.StateMachine.Stage { return nil } } + .receive(on: processingQueue) .sink { [weak self] (source: WebSocketConnectionState.DisconnectionSource) in guard let self else { return } context.disconnectionSource = source @@ -248,6 +250,7 @@ extension WebRTCCoordinator.StateMachine.Stage { sfuAdapter? .publisher(eventType: Stream_Video_Sfu_Event_CallEnded.self) .log(.debug, subsystems: .sfu) { "Call ended with reason: \($0.reason)." } + .receive(on: processingQueue) .sink { [weak self] _ in guard let self else { return } transitionOrError(.leaving(context)) @@ -264,6 +267,7 @@ extension WebRTCCoordinator.StateMachine.Stage { sfuAdapter? .publisher(eventType: Stream_Video_Sfu_Event_Error.self) .filter { $0.reconnectStrategy == .migrate } + .receive(on: processingQueue) .sink { [weak self] _ in guard let self else { return } context.reconnectionStrategy = .migrate @@ -277,6 +281,7 @@ extension WebRTCCoordinator.StateMachine.Stage { sfuAdapter? .publisher(eventType: Stream_Video_Sfu_Event_GoAway.self) + .receive(on: processingQueue) .sink { [weak self] _ in guard let self else { return } context.reconnectionStrategy = .migrate @@ -300,6 +305,7 @@ extension WebRTCCoordinator.StateMachine.Stage { sfuAdapter? .publisher(eventType: Stream_Video_Sfu_Event_Error.self) .filter { $0.reconnectStrategy == .disconnect } + .receive(on: processingQueue) .sink { [weak self] _ in guard let self else { return } transitionOrError(.leaving(context)) @@ -314,6 +320,7 @@ extension WebRTCCoordinator.StateMachine.Stage { sfuAdapter? .publisher(eventType: Stream_Video_Sfu_Event_Error.self) .filter { $0.error.code == .participantSignalLost } + .receive(on: processingQueue) .sink { [weak self] _ in guard let self else { return } context.reconnectionStrategy = .fast( @@ -332,6 +339,7 @@ extension WebRTCCoordinator.StateMachine.Stage { let sfuAdapter = await context.coordinator?.stateAdapter.sfuAdapter sfuAdapter? .publisher(eventType: Stream_Video_Sfu_Event_HealthCheckResponse.self) + .receive(on: processingQueue) .sink { [weak self] _ in self?.context.lastHealthCheckReceivedAt = .init() } @@ -348,6 +356,7 @@ extension WebRTCCoordinator.StateMachine.Stage { } return abs($0.timeIntervalSinceNow) > timeout } + .receive(on: processingQueue) .sink { [weak self] lastHealthCheckReceivedAt in guard let self else { return @@ -377,6 +386,7 @@ extension WebRTCCoordinator.StateMachine.Stage { ) } .log(.debug, subsystems: .webRTC) { "Reconnection strategy updated to \($0)." } + .receive(on: processingQueue) .sink { [weak self] in self?.context.reconnectionStrategy = $0 } .store(in: disposableBag) } @@ -396,6 +406,7 @@ extension WebRTCCoordinator.StateMachine.Stage { publisher .disconnectedPublisher .log(.debug, subsystems: .webRTC) { "PeerConnection of type: .publisher was disconnected. Will attempt rejoin." } + .receive(on: processingQueue) .sink { [weak self] in guard let self else { return } context.reconnectionStrategy = .rejoin @@ -406,6 +417,7 @@ extension WebRTCCoordinator.StateMachine.Stage { subscriber .disconnectedPublisher .log(.debug, subsystems: .webRTC) { "PeerConnection of type: .subscriber was disconnected. Will attempt rejoin." } + .receive(on: processingQueue) .sink { [weak self] in guard let self else { return } context.reconnectionStrategy = .rejoin @@ -467,11 +479,11 @@ extension WebRTCCoordinator.StateMachine.Stage { private func observeInternetConnection() { internetConnectionObserver .statusPublisher - .receive(on: DispatchQueue.main) .filter { $0 != .unknown } .log(.debug, subsystems: .webRTC) { "Internet connection status updated to \($0)" } .filter { !$0.isAvailable } .removeDuplicates() + .receive(on: processingQueue) .sinkTask(storeIn: disposableBag) { [weak self] in guard let self else { return } diff --git a/Sources/StreamVideoSwiftUI/Info.plist b/Sources/StreamVideoSwiftUI/Info.plist index a8ea0f1ec..ec8879a18 100644 --- a/Sources/StreamVideoSwiftUI/Info.plist +++ b/Sources/StreamVideoSwiftUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.38.1 + 1.38.2 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/Sources/StreamVideoUIKit/Info.plist b/Sources/StreamVideoUIKit/Info.plist index a8ea0f1ec..ec8879a18 100644 --- a/Sources/StreamVideoUIKit/Info.plist +++ b/Sources/StreamVideoUIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.38.1 + 1.38.2 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/StreamVideo-XCFramework.podspec b/StreamVideo-XCFramework.podspec index 1cf921a83..883d694e7 100644 --- a/StreamVideo-XCFramework.podspec +++ b/StreamVideo-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideo-XCFramework' - spec.version = '1.38.1' + spec.version = '1.38.2' spec.summary = 'StreamVideo iOS Video Client' spec.description = 'StreamVideo is the official Swift client for Stream Video, a service for building video applications.' diff --git a/StreamVideo.podspec b/StreamVideo.podspec index 6a06b391d..983484c48 100644 --- a/StreamVideo.podspec +++ b/StreamVideo.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideo' - spec.version = '1.38.1' + spec.version = '1.38.2' spec.summary = 'StreamVideo iOS Video Client' spec.description = 'StreamVideo is the official Swift client for Stream Video, a service for building video applications.' diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 567b9afae..7628dedf5 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -510,6 +510,7 @@ 40A0E9622B88D3DC0089E8D3 /* UIInterfaceOrientation+CGOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A0E9612B88D3DC0089E8D3 /* UIInterfaceOrientation+CGOrientation.swift */; }; 40A0E9682B88E04D0089E8D3 /* CIImage_Resize_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A0E9672B88E04D0089E8D3 /* CIImage_Resize_Tests.swift */; }; 40A0FFC02EA6418000F39D8F /* Sequence+AsyncReduce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A0FFBF2EA6418000F39D8F /* Sequence+AsyncReduce.swift */; }; + 40A481472EF1706200369D6E /* PerformanceMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A481462EF1706200369D6E /* PerformanceMetrics.swift */; }; 40A7C5B52E099B4600EEDF9C /* ParticipantEventResetAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A7C5B22E099B1000EEDF9C /* ParticipantEventResetAdapter.swift */; }; 40A7C5B82E099D6200EEDF9C /* ParticipantEventResetAdapter_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A7C5B72E099D6200EEDF9C /* ParticipantEventResetAdapter_Tests.swift */; }; 40A9416E2B4D959F006D6965 /* StreamPictureInPictureAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A9416D2B4D959F006D6965 /* StreamPictureInPictureAdapter.swift */; }; @@ -2243,6 +2244,7 @@ 40A0E9612B88D3DC0089E8D3 /* UIInterfaceOrientation+CGOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIInterfaceOrientation+CGOrientation.swift"; sourceTree = ""; }; 40A0E9672B88E04D0089E8D3 /* CIImage_Resize_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImage_Resize_Tests.swift; sourceTree = ""; }; 40A0FFBF2EA6418000F39D8F /* Sequence+AsyncReduce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+AsyncReduce.swift"; sourceTree = ""; }; + 40A481462EF1706200369D6E /* PerformanceMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMetrics.swift; sourceTree = ""; }; 40A7C5B22E099B1000EEDF9C /* ParticipantEventResetAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantEventResetAdapter.swift; sourceTree = ""; }; 40A7C5B72E099D6200EEDF9C /* ParticipantEventResetAdapter_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantEventResetAdapter_Tests.swift; sourceTree = ""; }; 40A9416D2B4D959F006D6965 /* StreamPictureInPictureAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPictureInPictureAdapter.swift; sourceTree = ""; }; @@ -4985,6 +4987,14 @@ path = Extensions; sourceTree = ""; }; + 40A481452EF1705A00369D6E /* XCTest Performance */ = { + isa = PBXGroup; + children = ( + 40A481462EF1706200369D6E /* PerformanceMetrics.swift */, + ); + path = "XCTest Performance"; + sourceTree = ""; + }; 40A7C5B42E099B1600EEDF9C /* ParticipantEventResetAdapter */ = { isa = PBXGroup; children = ( @@ -6743,6 +6753,7 @@ 842747F429EEDACB00E063AD /* Utils */ = { isa = PBXGroup; children = ( + 40A481452EF1705A00369D6E /* XCTest Performance */, 40B8FFCC2EC394D30061E3F6 /* Battery */, 40064BD72E5C88DC007CDB33 /* PermissionStore */, 40C71B572E5355F800733BF6 /* Store */, @@ -9491,6 +9502,7 @@ 403FB1512BFE1AA90047A696 /* CallStateMachine_Tests.swift in Sources */, 406B3C532C92007900FC93A1 /* WebRTCCoordinatorStateMachine_ConnectedStageTests.swift in Sources */, 40B48C172D14C97F002C4EAB /* CGSize_DefaultValuesTests.swift in Sources */, + 40A481472EF1706200369D6E /* PerformanceMetrics.swift in Sources */, 404A81342DA3CB66001F7FA8 /* CallStateMachine_RejectedStageTests.swift in Sources */, 40B48C342D14D3E6002C4EAB /* StreamVideoSfuSignalTrackSubscriptionDetails_ConvenienceTests.swift in Sources */, 405616F32E0C0E7200442FF2 /* ICEConnectionStateAdapter_Tests.swift in Sources */, diff --git a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/72BCA2C7-BCAB-4F29-B5A2-C1B720A9D6C7.plist b/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/72BCA2C7-BCAB-4F29-B5A2-C1B720A9D6C7.plist deleted file mode 100644 index 838e7a85e..000000000 --- a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/72BCA2C7-BCAB-4F29-B5A2-C1B720A9D6C7.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - classNames - - CallFlow_PerformanceTests - - test_performance_with2Participants() - - com.apple.dt.XCTMetric_Memory-io.getstream.iOS.VideoDemoApp.physical_absolute - - baselineAverage - 125000.000000 - baselineIntegrationDisplayName - Local Baseline - - - - - - diff --git a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/A871F8A3-784B-4A04-8235-A22FE4A40D80.plist b/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/A871F8A3-784B-4A04-8235-A22FE4A40D80.plist deleted file mode 100644 index baec3a88d..000000000 --- a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/A871F8A3-784B-4A04-8235-A22FE4A40D80.plist +++ /dev/null @@ -1,36 +0,0 @@ - - - - - classNames - - CallFlow_PerformanceTests - - test_performance_with2Participants() - - com.apple.dt.XCTMetric_Memory-io.getstream.iOS.VideoDemoApp.physical - - baselineAverage - 414000.000000 - baselineIntegrationDisplayName - Local Baseline - - com.apple.dt.XCTMetric_Memory-io.getstream.iOS.VideoDemoApp.physical_absolute - - baselineAverage - 414000.000000 - baselineIntegrationDisplayName - Local Baseline - - com.apple.dt.XCTMetric_Memory-io.getstream.iOS.VideoDemoApp.physical_peak - - baselineAverage - 802000.000000 - baselineIntegrationDisplayName - Local Baseline - - - - - - diff --git a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/Info.plist b/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/Info.plist deleted file mode 100644 index 5951734d9..000000000 --- a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/82392D502993C9E100941435.xcbaseline/Info.plist +++ /dev/null @@ -1,52 +0,0 @@ - - - - - runDestinationsByUUID - - 72BCA2C7-BCAB-4F29-B5A2-C1B720A9D6C7 - - targetArchitecture - arm64 - targetDevice - - modelCode - iPhone13,1 - platformIdentifier - com.apple.platform.iphoneos - - - A871F8A3-784B-4A04-8235-A22FE4A40D80 - - localComputer - - busSpeedInMHz - 0 - cpuCount - 1 - cpuKind - Apple M1 Pro - cpuSpeedInMHz - 0 - logicalCPUCoresPerPackage - 10 - modelCode - MacBookPro18,1 - physicalCPUCoresPerPackage - 10 - platformIdentifier - com.apple.platform.macosx - - targetArchitecture - arm64 - targetDevice - - modelCode - iPhone18,1 - platformIdentifier - com.apple.platform.iphonesimulator - - - - - diff --git a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/05BF3974-39D5-4CF1-A0F9-3E40F2AA51F3.plist b/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/05BF3974-39D5-4CF1-A0F9-3E40F2AA51F3.plist deleted file mode 100644 index b83c1f69e..000000000 --- a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/05BF3974-39D5-4CF1-A0F9-3E40F2AA51F3.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - classNames - - CIImage_Resize_Tests - - testResizePerformance() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 0.020000 - baselineIntegrationDisplayName - Local Baseline - - - - - - diff --git a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/405B1570-F1EC-4638-9C96-FA56CB97DF5B.plist b/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/405B1570-F1EC-4638-9C96-FA56CB97DF5B.plist deleted file mode 100644 index 1ea8c9de0..000000000 --- a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/405B1570-F1EC-4638-9C96-FA56CB97DF5B.plist +++ /dev/null @@ -1,82 +0,0 @@ - - - - - classNames - - Store_PerformanceTests - - test_measureComplexStateUpdates() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 2.060000 - baselineIntegrationDisplayName - Local Baseline - - - test_measureDispatchThroughput() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 1.061101 - baselineIntegrationDisplayName - Local Baseline - - - test_measureDispatchWithDelaysThroughput() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 1.027871 - baselineIntegrationDisplayName - Local Baseline - - - test_measureMiddlewareImpact() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 1.038768 - baselineIntegrationDisplayName - Local Baseline - - - test_measurePublisherPerformance() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 1.043790 - baselineIntegrationDisplayName - Local Baseline - - - test_measureSyncDispatchLatency() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 0.003969 - baselineIntegrationDisplayName - Local Baseline - - - test_memoryUsageWithLargeState() - - com.apple.XCTPerformanceMetric_WallClockTime - - baselineAverage - 1.059265 - baselineIntegrationDisplayName - Local Baseline - - - - - - diff --git a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/Info.plist b/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/Info.plist deleted file mode 100644 index e1fa8dbd3..000000000 --- a/StreamVideo.xcodeproj/xcshareddata/xcbaselines/84F737F3287C13AD00A363F4.xcbaseline/Info.plist +++ /dev/null @@ -1,71 +0,0 @@ - - - - - runDestinationsByUUID - - 05BF3974-39D5-4CF1-A0F9-3E40F2AA51F3 - - localComputer - - busSpeedInMHz - 0 - cpuCount - 1 - cpuKind - Apple M1 Pro - cpuSpeedInMHz - 0 - logicalCPUCoresPerPackage - 10 - modelCode - MacBookPro18,1 - physicalCPUCoresPerPackage - 10 - platformIdentifier - com.apple.platform.macosx - - targetArchitecture - arm64 - targetDevice - - modelCode - iPhone16,1 - platformIdentifier - com.apple.platform.iphonesimulator - - - 405B1570-F1EC-4638-9C96-FA56CB97DF5B - - localComputer - - busSpeedInMHz - 0 - cpuCount - 1 - cpuKind - Apple M1 Pro - cpuSpeedInMHz - 0 - logicalCPUCoresPerPackage - 10 - modelCode - MacBookPro18,1 - physicalCPUCoresPerPackage - 10 - platformIdentifier - com.apple.platform.macosx - - targetArchitecture - arm64 - targetDevice - - modelCode - iPhone17,1 - platformIdentifier - com.apple.platform.iphonesimulator - - - - - diff --git a/StreamVideoArtifacts.json b/StreamVideoArtifacts.json index b9a9558da..703feccf7 100644 --- a/StreamVideoArtifacts.json +++ b/StreamVideoArtifacts.json @@ -1 +1 @@ -{"0.4.2":"https://github.com/GetStream/stream-video-swift/releases/download/0.4.2/StreamVideo-All.zip","0.5.0":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.0/StreamVideo-All.zip","0.5.1":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.1/StreamVideo-All.zip","0.5.2":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.2/StreamVideo-All.zip","0.5.3":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.3/StreamVideo-All.zip","1.0.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.0/StreamVideo-All.zip","1.0.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.1/StreamVideo-All.zip","1.0.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.2/StreamVideo-All.zip","1.0.3":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.3/StreamVideo-All.zip","1.0.4":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.4/StreamVideo-All.zip","1.0.5":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.5/StreamVideo-All.zip","1.0.6":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.6/StreamVideo-All.zip","1.0.7":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.7/StreamVideo-All.zip","1.0.8":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.8/StreamVideo-All.zip","1.0.9":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.9/StreamVideo-All.zip","1.10.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.10.0/StreamVideo-All.zip","1.11.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.11.0/StreamVideo-All.zip","1.12.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.12.0/StreamVideo-All.zip","1.13.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.13.0/StreamVideo-All.zip","1.14.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.14.0/StreamVideo-All.zip","1.14.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.14.1/StreamVideo-All.zip","1.15.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.15.0/StreamVideo-All.zip","1.16.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.16.0/StreamVideo-All.zip","1.17.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.17.0/StreamVideo-All.zip","1.18.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.18.0/StreamVideo-All.zip","1.19.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.19.0/StreamVideo-All.zip","1.19.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.19.1/StreamVideo-All.zip","1.19.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.19.2/StreamVideo-All.zip","1.20.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.20.0/StreamVideo-All.zip","1.21.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.21.0/StreamVideo-All.zip","1.21.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.21.1/StreamVideo-All.zip","1.22.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.22.0/StreamVideo-All.zip","1.22.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.22.1/StreamVideo-All.zip","1.22.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.22.2/StreamVideo-All.zip","1.24.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.24.0/StreamVideo-All.zip","1.25.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.25.0/StreamVideo-All.zip","1.26.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.26.0/StreamVideo-All.zip","1.27.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.27.0/StreamVideo-All.zip","1.27.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.27.1/StreamVideo-All.zip","1.27.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.27.2/StreamVideo-All.zip","1.28.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.28.0/StreamVideo-All.zip","1.28.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.28.1/StreamVideo-All.zip","1.29.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.29.0/StreamVideo-All.zip","1.29.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.29.1/StreamVideo-All.zip","1.30.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.30.0/StreamVideo-All.zip","1.31.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.31.0/StreamVideo-All.zip","1.32.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.32.0/StreamVideo-All.zip","1.33.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.33.0/StreamVideo-All.zip","1.34.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.34.0/StreamVideo-All.zip","1.34.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.34.1/StreamVideo-All.zip","1.34.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.34.2/StreamVideo-All.zip","1.35.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.35.0/StreamVideo-All.zip","1.36.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.36.0/StreamVideo-All.zip","1.37.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.37.0/StreamVideo-All.zip","1.38.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.38.0/StreamVideo-All.zip","1.38.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.38.1/StreamVideo-All.zip"} \ No newline at end of file +{"0.4.2":"https://github.com/GetStream/stream-video-swift/releases/download/0.4.2/StreamVideo-All.zip","0.5.0":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.0/StreamVideo-All.zip","0.5.1":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.1/StreamVideo-All.zip","0.5.2":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.2/StreamVideo-All.zip","0.5.3":"https://github.com/GetStream/stream-video-swift/releases/download/0.5.3/StreamVideo-All.zip","1.0.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.0/StreamVideo-All.zip","1.0.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.1/StreamVideo-All.zip","1.0.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.2/StreamVideo-All.zip","1.0.3":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.3/StreamVideo-All.zip","1.0.4":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.4/StreamVideo-All.zip","1.0.5":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.5/StreamVideo-All.zip","1.0.6":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.6/StreamVideo-All.zip","1.0.7":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.7/StreamVideo-All.zip","1.0.8":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.8/StreamVideo-All.zip","1.0.9":"https://github.com/GetStream/stream-video-swift/releases/download/1.0.9/StreamVideo-All.zip","1.10.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.10.0/StreamVideo-All.zip","1.11.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.11.0/StreamVideo-All.zip","1.12.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.12.0/StreamVideo-All.zip","1.13.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.13.0/StreamVideo-All.zip","1.14.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.14.0/StreamVideo-All.zip","1.14.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.14.1/StreamVideo-All.zip","1.15.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.15.0/StreamVideo-All.zip","1.16.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.16.0/StreamVideo-All.zip","1.17.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.17.0/StreamVideo-All.zip","1.18.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.18.0/StreamVideo-All.zip","1.19.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.19.0/StreamVideo-All.zip","1.19.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.19.1/StreamVideo-All.zip","1.19.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.19.2/StreamVideo-All.zip","1.20.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.20.0/StreamVideo-All.zip","1.21.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.21.0/StreamVideo-All.zip","1.21.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.21.1/StreamVideo-All.zip","1.22.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.22.0/StreamVideo-All.zip","1.22.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.22.1/StreamVideo-All.zip","1.22.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.22.2/StreamVideo-All.zip","1.24.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.24.0/StreamVideo-All.zip","1.25.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.25.0/StreamVideo-All.zip","1.26.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.26.0/StreamVideo-All.zip","1.27.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.27.0/StreamVideo-All.zip","1.27.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.27.1/StreamVideo-All.zip","1.27.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.27.2/StreamVideo-All.zip","1.28.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.28.0/StreamVideo-All.zip","1.28.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.28.1/StreamVideo-All.zip","1.29.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.29.0/StreamVideo-All.zip","1.29.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.29.1/StreamVideo-All.zip","1.30.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.30.0/StreamVideo-All.zip","1.31.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.31.0/StreamVideo-All.zip","1.32.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.32.0/StreamVideo-All.zip","1.33.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.33.0/StreamVideo-All.zip","1.34.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.34.0/StreamVideo-All.zip","1.34.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.34.1/StreamVideo-All.zip","1.34.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.34.2/StreamVideo-All.zip","1.35.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.35.0/StreamVideo-All.zip","1.36.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.36.0/StreamVideo-All.zip","1.37.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.37.0/StreamVideo-All.zip","1.38.0":"https://github.com/GetStream/stream-video-swift/releases/download/1.38.0/StreamVideo-All.zip","1.38.1":"https://github.com/GetStream/stream-video-swift/releases/download/1.38.1/StreamVideo-All.zip","1.38.2":"https://github.com/GetStream/stream-video-swift/releases/download/1.38.2/StreamVideo-All.zip"} \ No newline at end of file diff --git a/StreamVideoSwiftUI-XCFramework.podspec b/StreamVideoSwiftUI-XCFramework.podspec index 92596718e..f44e3e9d5 100644 --- a/StreamVideoSwiftUI-XCFramework.podspec +++ b/StreamVideoSwiftUI-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoSwiftUI-XCFramework' - spec.version = '1.38.1' + spec.version = '1.38.2' spec.summary = 'StreamVideo SwiftUI Video Components' spec.description = 'StreamVideoSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamVideo SDK.' diff --git a/StreamVideoSwiftUI.podspec b/StreamVideoSwiftUI.podspec index fe30aa732..f6f99fa62 100644 --- a/StreamVideoSwiftUI.podspec +++ b/StreamVideoSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoSwiftUI' - spec.version = '1.38.1' + spec.version = '1.38.2' spec.summary = 'StreamVideo SwiftUI Video Components' spec.description = 'StreamVideoSwiftUI SDK offers flexible SwiftUI components able to display data provided by StreamVideo SDK.' diff --git a/StreamVideoTests/HTTPClient/InternetConnection_Tests.swift b/StreamVideoTests/HTTPClient/InternetConnection_Tests.swift index 94de5aa7a..e40ccd2e0 100644 --- a/StreamVideoTests/HTTPClient/InternetConnection_Tests.swift +++ b/StreamVideoTests/HTTPClient/InternetConnection_Tests.swift @@ -6,30 +6,27 @@ import XCTest final class InternetConnection_Tests: XCTestCase, @unchecked Sendable { - var monitor: InternetConnectionMonitor_Mock! - var internetConnection: InternetConnection! + private var monitor: InternetConnectionMonitor_Mock! = .init() + private lazy var subject: InternetConnection! = .init(monitor: monitor) - override func setUp() { - super.setUp() - monitor = InternetConnectionMonitor_Mock() - internetConnection = InternetConnection(monitor: monitor) + override func setUp() async throws { + try await super.setUp() + _ = subject + await fulfillment { self.subject.status == .available(.great) } } override func tearDown() { - AssertAsync.canBeReleased(&internetConnection) - AssertAsync.canBeReleased(&monitor) - monitor = nil - internetConnection = nil + subject = nil super.tearDown() } func test_internetConnection_init() { // Assert status matches ther monitor - XCTAssertEqual(internetConnection.status, monitor.status) + XCTAssertEqual(subject.status, monitor.status) // Assert internet connection is set as a delegate - XCTAssertTrue(monitor.delegate === internetConnection) + XCTAssertTrue(monitor.delegate === subject) } func test_internetConnection_postsStatusAndAvailabilityNotifications_whenAvailabilityChanges() { @@ -43,12 +40,12 @@ final class InternetConnection_Tests: XCTestCase, @unchecked Sendable { let notificationExpectations = [ expectation( forNotification: .internetConnectionStatusDidChange, - object: internetConnection, + object: subject, handler: { $0.internetConnectionStatus == newStatus } ), expectation( forNotification: .internetConnectionAvailabilityDidChange, - object: internetConnection, + object: subject, handler: { $0.internetConnectionStatus == newStatus } ) ] @@ -57,7 +54,7 @@ final class InternetConnection_Tests: XCTestCase, @unchecked Sendable { monitor.status = newStatus // Assert status is updated - XCTAssertEqual(internetConnection.status, newStatus) + XCTAssertEqual(subject.status, newStatus) // Assert both notifications are posted wait(for: notificationExpectations, timeout: defaultTimeout) @@ -73,7 +70,7 @@ final class InternetConnection_Tests: XCTestCase, @unchecked Sendable { // Set up expectation for a notification let notificationExpectation = expectation( forNotification: .internetConnectionStatusDidChange, - object: internetConnection, + object: subject, handler: { $0.internetConnectionStatus == newStatus } ) @@ -81,7 +78,7 @@ final class InternetConnection_Tests: XCTestCase, @unchecked Sendable { monitor.status = newStatus // Assert status is updated - XCTAssertEqual(internetConnection.status, newStatus) + XCTAssertEqual(subject.status, newStatus) // Assert both notifications are posted wait(for: [notificationExpectation], timeout: defaultTimeout) @@ -90,7 +87,7 @@ final class InternetConnection_Tests: XCTestCase, @unchecked Sendable { func test_internetConnection_stopsMonitorWhenDeallocated() throws { assert(monitor.isStarted) - internetConnection = nil + subject = nil XCTAssertFalse(monitor.isStarted) } } diff --git a/StreamVideoTests/IntegrationTests/CallCRUDTests.swift b/StreamVideoTests/IntegrationTests/CallCRUDTests.swift index 804f4d498..3d2447bb1 100644 --- a/StreamVideoTests/IntegrationTests/CallCRUDTests.swift +++ b/StreamVideoTests/IntegrationTests/CallCRUDTests.swift @@ -7,696 +7,696 @@ import Foundation @testable import StreamVideo import XCTest -final class CallCRUDTests: IntegrationTest, @unchecked Sendable { - - let user1 = "thierry" - let user2 = "tommaso" - let defaultCallType = "default" - let apiErrorCode = 16 - let randomCallId = UUID().uuidString - let userIdKey = MemberRequest.CodingKeys.userId.rawValue - - func customWait(nanoseconds duration: UInt64 = 3_000_000_000) async throws { - try await Task.sleep(nanoseconds: duration) - } - - func waitForCapability( - _ capability: OwnCapability, - on call: Call, - granted: Bool = true, - timeout: Double = 20 - ) async -> Bool { - let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 - var userHasRequiredCapability = !granted - while userHasRequiredCapability != granted && endTime > Date().timeIntervalSince1970 * 1000 { - print("Waiting for \(capability.rawValue)") - userHasRequiredCapability = await call.currentUserHasCapability(capability) - } - return userHasRequiredCapability - } - - func waitForAudio( - on call: Call, - timeout: Double = 20 - ) async -> Bool { - let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 - var usersHaveAudio = false - while !usersHaveAudio && endTime > Date().timeIntervalSince1970 * 1000 { - print("Waiting for Audio") - let u1 = await call.state.participants.first!.hasAudio - let u2 = await call.state.participants.last!.hasAudio - usersHaveAudio = u1 && u2 - } - return usersHaveAudio - } - - func waitForAudioLoss( - on call: Call, - timeout: Double = 20 - ) async -> Bool { - let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 - var usersLostAudio = false - while !usersLostAudio && endTime > Date().timeIntervalSince1970 * 1000 { - print("Waiting for Audio Loss") - let u1 = await call.state.participants.first!.hasAudio - let u2 = await call.state.participants.last!.hasAudio - usersLostAudio = u1 == false && u2 == false - } - return usersLostAudio - } - - func waitForPinning( - firstUserCall: Call, - secondUserCall: Call, - timeout: Double = 20 - ) async { - let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 - var userIsPinned = false - while !userIsPinned && endTime > Date().timeIntervalSince1970 * 1000 { - print("Waiting for Pinning") - let pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin - userIsPinned = pin != nil - } - } - - func waitForUnpinning( - firstUserCall: Call, - secondUserCall: Call, - timeout: Double = 20 - ) async { - let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 - var userIsUnpinned = false - while !userIsUnpinned && endTime > Date().timeIntervalSince1970 * 1000 { - print("Waiting for Unpinning") - let pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin - userIsUnpinned = pin == nil - } - } - - func test_callCreateAndUpdate() async throws { - let colorKey = "color" - let red: RawJSON = "red" - let blue: RawJSON = "blue" - - let call = client.call(callType: defaultCallType, callId: randomCallId) - - let response = try await call.create(custom: [colorKey: red]) - XCTAssertEqual(response.custom[colorKey], red) - - await assertNext(call.state.$custom) { v in - guard let newColor = v[colorKey]?.stringValue else { - return false - } - return newColor == red.stringValue - } - - let updateResponse = try await call.update(custom: [colorKey: blue]) - XCTAssertEqual(updateResponse.call.custom[colorKey], blue) - - await assertNext(call.state.$custom) { v in - v[colorKey] == blue - } - } - - func test_getCallMissingId() async throws { - let call = client.call(callType: defaultCallType, callId: randomCallId) - let apiErr = await XCTAssertThrowsErrorAsync { - _ = try await call.get() - } - guard let apiErr = apiErr as? APIError else { - XCTAssert((apiErr as Any) is APIError) - return - } - XCTAssertEqual(apiErr.code, apiErrorCode) - - let expectedErrMessage = "GetCall failed with error: \"Can't find call with id \(call.cId)\"" - XCTAssertEqual(apiErr.message, expectedErrMessage) - } - - func test_getCallWrongType() async throws { - let wrongCallType = "bananas" - let call = client.call(callType: wrongCallType, callId: randomCallId) - let apiErr = await XCTAssertThrowsErrorAsync { - _ = try await call.get() - return - } - guard let apiErr = apiErr as? APIError else { - XCTAssert((apiErr as Any) is APIError) - return - } - XCTAssertEqual(apiErr.code, apiErrorCode) - - let expectedErrMessage = "\(wrongCallType): call type does not exist" - XCTAssertTrue(apiErr.message.localizedStandardContains(expectedErrMessage)) - } - - func test_sendCustomEvent() async throws { - let customEventKey = "test" - let customEventValue = "asd" - - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create() - - let subscription = call.subscribe(for: CustomVideoEvent.self) - try await call.sendCustomEvent([customEventKey: .string(customEventValue)]) - - await assertNext(subscription) { ev in - ev.custom[customEventKey]?.stringValue == customEventValue - } - } - - func test_createCallWithMembers() async throws { - let roleKey = "role" - let roleValue = "CEO" - let membersGroup = "stars" - let membersCount: Double = 3 - - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1]) - - await assertNext(call.state.$members) { v in - v.count == 1 && v[0].id == self.user1 - } - - try await call - .updateMembers( - members: [ - .init( - custom: [ - membersGroup: .number(membersCount) - ], userId: user1 - ) - ] - ) - - await fulfilmentInMainActor { - if let member = call.state.members.first { - return member.id == self.user1 - && member.customData[membersGroup]?.numberValue == membersCount - } else { - return false - } - } - - try await call.removeMembers(ids: [user1]) - - await fulfilmentInMainActor { call.state.members.isEmpty } - - try await call.addMembers( - members: [ - .init( - custom: [roleKey: .string(roleValue)], - userId: user1 - ) - ] - ) - - await fulfilmentInMainActor { - if let member = call.state.members.first { - return member.id == self.user1 - && member.customData[roleKey]?.stringValue == roleValue - } else { - return false - } - } - } - - func test_paginateCallWithMembers() async throws { - let call1 = client.call(callType: defaultCallType, callId: randomCallId) - try await call1.create(memberIds: [user1]) - - let call2 = client.call(callType: call1.callType, callId: call1.callId) - _ = try await call2.get(membersLimit: 1) - - await fulfilmentInMainActor { call1.state.members.count == 1 } - - var membersResponse = try await call2.queryMembers() - XCTAssertEqual(1, membersResponse.members.count) - - membersResponse = try await call2.queryMembers(filters: [userIdKey: .string(user1)]) - XCTAssertEqual(1, membersResponse.members.count) - - membersResponse = try await call2.queryMembers(filters: [userIdKey: .string(user2)]) - XCTAssertEqual(0, membersResponse.members.count) - - let secondUserClient = try await makeClient(for: user2) - try await secondUserClient.connect() - - // add to call2 so we can test that the other call object is updated via WS events - try await call2.addMembers(ids: [user2]) - await fulfilmentInMainActor { call1.state.members.count == 2 } - - membersResponse = try await call2.queryMembers(filters: [userIdKey: .string(user2)]) - XCTAssertEqual(1, membersResponse.members.count) - - membersResponse = try await call2.queryMembers(limit: 1) - XCTAssertEqual(1, membersResponse.members.count) - XCTAssertEqual(user2, membersResponse.members.first?.userId) - - membersResponse = try await call2.queryMembers(next: membersResponse.next) - XCTAssertEqual(1, membersResponse.members.count) - XCTAssertEqual(user1, membersResponse.members.first?.userId) - - await fulfilmentInMainActor { - call2.state.members.count == 2 - && call2.state.members.first?.id == self.user2 - } - } - - @MainActor - func test_queryChannels() async throws { - let colorKey = "color" - let blue: RawJSON = "blue" - - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1]) - - let (calls, _) = try await client.queryCalls( - filters: [CallSortField.cid.rawValue: .string(call.cId)], - watch: true - ) - XCTAssertEqual(1, calls.count) - var fetchedCall = try XCTUnwrap(calls.first) - XCTAssertEqual(call.cId, fetchedCall.cId) - - // changes to a watched call via query call should propagate as usual to the state - let updateResponse = try await call.update(custom: [colorKey: blue]) - XCTAssertEqual(updateResponse.call.custom[colorKey], blue) - - await fulfilmentInMainActor { fetchedCall.state.custom[colorKey] == blue } - - let (secondTry, _) = try await client.queryCalls( - filters: [ - CallSortField.endedAt.rawValue: .nil, - CallSortField.cid.rawValue: .string(call.cId) - ] - ) - XCTAssertEqual(1, secondTry.count) - fetchedCall = try XCTUnwrap(secondTry.first) - XCTAssertEqual(call.cId, fetchedCall.cId) - - try await call.end() - - let (thirdTry, _) = try await client.queryCalls( - filters: [ - CallSortField.endedAt.rawValue: .nil, - CallSortField.cid.rawValue: .string(call.cId) - ] - ) - XCTAssertEqual(0, thirdTry.count) - - await fulfilmentInMainActor { fetchedCall.state.endedAt != nil } - } - - func test_sendReaction() async throws { - let reactionType1 = "happy" - let reactionType2 = "happyy" - let reactionType3 = "happyyy" - let emojiCode = ":smile:" - let customReactionKey = "test" - let customReactionValue = "asd" - - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1]) - - let specificSub = call.subscribe(for: CallReactionEvent.self) - - _ = try await call.sendReaction(type: reactionType1) - - await assertNext(specificSub) { ev in - ev.reaction.type == reactionType1 - } - - _ = try await call.sendReaction(type: reactionType2, emojiCode: emojiCode) - await assertNext(specificSub) { ev in - ev.reaction.type == reactionType2 && ev.reaction.emojiCode == emojiCode - } - - _ = try await call.sendReaction( - type: reactionType3, - custom: [customReactionKey: .string(customReactionValue)] - ) - await assertNext(specificSub) { ev in - ev.reaction.type == reactionType3 && ev.reaction.custom?[customReactionKey]?.stringValue == customReactionValue - } - } - - func test_requestPermissionDiscard() async throws { - let firstUserCall = client.call( - callType: String.audioRoom, - callId: randomCallId - ) - try await firstUserCall.create(memberIds: [user1]) - - let secondUserClient = try await makeClient(for: user2) - try await secondUserClient.connect() - let secondUserCall = secondUserClient.call( - callType: String.audioRoom, - callId: firstUserCall.callId - ) - - _ = try await secondUserCall.get() - var hasAudioCapability = await secondUserCall.currentUserHasCapability(.sendAudio) - XCTAssertFalse(hasAudioCapability) - var hasVideoCapability = await secondUserCall.currentUserHasCapability(.sendVideo) - XCTAssertFalse(hasVideoCapability) - - try await secondUserCall.request(permissions: [.sendAudio]) - - await assertNext(firstUserCall.state.$permissionRequests) { value in - value.count == 1 && value.first?.permission == Permission.sendAudio.rawValue - } - if let p = await firstUserCall.state.permissionRequests.first { - p.reject() - } - - // Test: permission requests list is now empty - await assertNext(firstUserCall.state.$permissionRequests) { value in - value.isEmpty - } - - hasAudioCapability = await secondUserCall.currentUserHasCapability(.sendAudio) - XCTAssertFalse(hasAudioCapability) - hasVideoCapability = await secondUserCall.currentUserHasCapability(.sendVideo) - XCTAssertFalse(hasVideoCapability) - } - - func test_muteUserById() async throws { - throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") - - let firstUserCall = client.call(callType: String.audioRoom, callId: randomCallId) - try await firstUserCall.create(memberIds: [user1, user2]) - try await firstUserCall.goLive() - - let secondUserClient = try await makeClient(for: user2) - let secondUserCall = secondUserClient.call( - callType: String.audioRoom, - callId: firstUserCall.callId - ) - - try await firstUserCall.join() - try await customWait() - - try await firstUserCall.microphone.enable() - try await customWait() - - try await secondUserCall.join() - try await customWait() - - try await firstUserCall.grant(permissions: [.sendAudio], for: user2) - try await customWait() - - try await secondUserCall.microphone.enable() - try await customWait() - - let everyoneIsUnmutedOnTheFirstCall = await waitForAudio(on: firstUserCall) - let everyoneIsUnmutedOnTheSecondCall = await waitForAudio(on: secondUserCall) - XCTAssertTrue(everyoneIsUnmutedOnTheFirstCall, "Everyone should be unmuted on creator's call") - XCTAssertTrue(everyoneIsUnmutedOnTheSecondCall, "Everyone should be unmuted on participant's call") - - for userId in [user1, user2] { - try await firstUserCall.mute(userId: userId) - } - try await customWait() - - let everyoneIsMutedOnTheFirstCall = await waitForAudioLoss(on: firstUserCall) - let everyoneIsMutedOnTheSecondCall = await waitForAudioLoss(on: secondUserCall) - XCTAssertTrue(everyoneIsMutedOnTheFirstCall, "Everyone should be muted on creator's call") - XCTAssertTrue(everyoneIsMutedOnTheSecondCall, "Everyone should be muted on participant's call") - } - - func test_muteAllUsers() async throws { - throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") - - let firstUserCall = client.call(callType: String.audioRoom, callId: randomCallId) - try await firstUserCall.create(memberIds: [user1, user2]) - try await firstUserCall.goLive() - - let secondUserClient = try await makeClient(for: user2) - let secondUserCall = secondUserClient.call( - callType: String.audioRoom, - callId: firstUserCall.callId - ) - - try await firstUserCall.join() - try await customWait() - - try await firstUserCall.microphone.enable() - try await customWait() - - try await secondUserCall.join() - try await customWait() - - try await firstUserCall.grant(permissions: [.sendAudio], for: user2) - try await customWait() - - try await secondUserCall.microphone.enable() - try await customWait() - - let everyoneIsUnmutedOnTheFirstCall = await waitForAudio(on: firstUserCall) - let everyoneIsUnmutedOnTheSecondCall = await waitForAudio(on: secondUserCall) - XCTAssertTrue(everyoneIsUnmutedOnTheFirstCall, "Everyone should be unmuted on creator's call") - XCTAssertTrue(everyoneIsUnmutedOnTheSecondCall, "Everyone should be unmuted on participant's call") - - try await firstUserCall.muteAllUsers() - try await customWait() - - let everyoneIsMutedOnTheFirstCall = await waitForAudioLoss(on: firstUserCall) - let everyoneIsMutedOnTheSecondCall = await waitForAudioLoss(on: secondUserCall) - XCTAssertTrue(everyoneIsMutedOnTheFirstCall, "Everyone should be muted on creator's call") - XCTAssertTrue(everyoneIsMutedOnTheSecondCall, "Everyone should be muted on participant's call") - } - - func test_blockAndUnblockUser() async throws { - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1, user2]) - try await call.blockUser(with: user2) - - var membersResponse = try await call.queryMembers() - XCTAssertEqual(2, membersResponse.members.count) - - var blockedUsers = try await call.get().call.blockedUserIds - XCTAssertEqual(blockedUsers, [user2]) - - try await call.unblockUser(with: user2) - - membersResponse = try await call.queryMembers() - XCTAssertEqual(2, membersResponse.members.count) - - blockedUsers = try await call.get().call.blockedUserIds - XCTAssertEqual(blockedUsers, []) - } - - func test_createCallWithMembersAndMemberIds() async throws { - let call = client.call(callType: defaultCallType, callId: randomCallId) - - var membersRequest = [MemberRequest]() - membersRequest.append(.init(userId: user2)) - - try await call.create(members: membersRequest, memberIds: [user1]) - let membersResponse = try await call.queryMembers() - - XCTAssertEqual(2, membersResponse.members.count) - } - - func test_grantPermissions() async throws { - throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") - - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1]) - - let expectedPermissions: [Permission] = [.sendAudio, .sendVideo, .screenshare] - try await call.revoke(permissions: expectedPermissions, for: user1) - - for permission in expectedPermissions { - let capability = try XCTUnwrap(OwnCapability(rawValue: permission.rawValue)) - let userHasRequiredCapability = await waitForCapability(capability, on: call, granted: false) - XCTAssertFalse(userHasRequiredCapability, "\(permission.rawValue) should not be granted") - } - - try await call.grant(permissions: expectedPermissions, for: user1) - - for permission in expectedPermissions { - let capability = try XCTUnwrap(OwnCapability(rawValue: permission.rawValue)) - let userHasRequiredCapability = await waitForCapability(capability, on: call) - XCTAssertTrue(userHasRequiredCapability, "\(permission.rawValue) permission should be granted") - } - } - - func test_grantPermissionsByRequest() async throws { - throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") - - let firstUserCall = client.call(callType: String.audioRoom, callId: randomCallId) - try await firstUserCall.create(memberIds: [user1, user2]) - - let secondUserClient = try await makeClient(for: user2) - let secondUserCall = secondUserClient.call( - callType: firstUserCall.callType, - callId: firstUserCall.callId - ) - - refreshStreamVideoProviderKey() - - try await firstUserCall.revoke(permissions: [.sendAudio], for: secondUserClient.user.id) - - var userHasUnexpectedCapability = await waitForCapability(.sendAudio, on: secondUserCall, granted: false) - XCTAssertFalse(userHasUnexpectedCapability) - - try await secondUserCall.request(permissions: [.sendAudio]) - - userHasUnexpectedCapability = await waitForCapability(.sendAudio, on: secondUserCall, granted: false) - XCTAssertFalse(userHasUnexpectedCapability) - - await assertNext(firstUserCall.state.$permissionRequests) { value in - value.count == 1 && value.first?.permission == Permission.sendAudio.rawValue - } - if let p = await firstUserCall.state.permissionRequests.first { - try await firstUserCall.grant(request: p) - } - - let userHasExpectedCapability = await waitForCapability(.sendAudio, on: secondUserCall) - XCTAssertTrue(userHasExpectedCapability) - } - - func test_acceptCall() async throws { - try await client.connect() - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1]) - try await call.ring() - try await customWait() - - var session = try await call.get().call.session - XCTAssertEqual(session?.acceptedBy.isEmpty, true, "Call should not be accepted yet") - - try await call.accept() - try await customWait() - - session = try await call.get().call.session - XCTAssertNotNil(session?.acceptedBy[client.user.id], "Call should be accepted by \(user1)") - } - - func test_notifyUser() async throws { - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1, user2]) - - let subscription = call.subscribe(for: CallNotificationEvent.self) - - try await call.notify() - try await customWait() - - await assertNext(subscription) { [user2] ev in - ev.members.first?.userId == user2 - } - } - - func test_setAndDeleteDevices() async throws { - let deviceId = UUID().uuidString - let voipDeviceId = UUID().uuidString - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1, user2]) - - try await call.streamVideo.setDevice(id: deviceId) - try await call.streamVideo.setVoipDevice(id: voipDeviceId) - try await customWait() - var listDevices = try await call.streamVideo.listDevices() - XCTAssertTrue(listDevices.contains(where: { $0.id == deviceId }), "Device should be added") - XCTAssertTrue(listDevices.contains(where: { $0.id == voipDeviceId }), "Voip device should be added") - - try await call.streamVideo.deleteDevice(id: deviceId) - try await call.streamVideo.deleteDevice(id: voipDeviceId) - try await customWait() - listDevices = try await call.streamVideo.listDevices() - XCTAssertFalse(listDevices.contains(where: { $0.id == deviceId }), "Device should be removed") - XCTAssertFalse(listDevices.contains(where: { $0.id == voipDeviceId }), "Voip device should be removed") - } - - func test_setAndDeleteVoipDevices() async throws { - throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") - - let deviceId = UUID().uuidString - let call = client.call(callType: defaultCallType, callId: randomCallId) - try await call.create(memberIds: [user1, user2]) - - try await call.streamVideo.setVoipDevice(id: deviceId) - try await customWait() - var listDevices = try await call.streamVideo.listDevices() - XCTAssertTrue(listDevices.contains(where: { $0.id == deviceId })) - - try await call.streamVideo.deleteDevice(id: deviceId) - try await customWait() - listDevices = try await call.streamVideo.listDevices() - XCTAssertFalse(listDevices.contains(where: { $0.id == deviceId })) - } - - func test_pinAndUnpinUser() async throws { - try await client.connect() - let firstUserCall = client.call(callType: .default, callId: randomCallId) - try await firstUserCall.create(memberIds: [user1, user2]) - - let secondUserClient = try await makeClient(for: user2) - let secondUserCall = secondUserClient.call( - callType: .default, - callId: firstUserCall.callId - ) - - try await firstUserCall.join() - try await customWait() - - try await secondUserCall.join() - try await customWait() - - _ = try await firstUserCall.pinForEveryone(userId: user2, sessionId: secondUserCall.state.sessionId) - await waitForPinning(firstUserCall: firstUserCall, secondUserCall: secondUserCall) - - var pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin - XCTAssertNotNil(pin) - XCTAssertEqual(pin?.isLocal, false) - - _ = try await firstUserCall.unpinForEveryone(userId: user2, sessionId: secondUserCall.state.sessionId) - await waitForUnpinning(firstUserCall: firstUserCall, secondUserCall: secondUserCall) - - pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin - XCTAssertNil(pin) - - try await firstUserCall.pin(sessionId: secondUserCall.state.sessionId) - await waitForPinning(firstUserCall: firstUserCall, secondUserCall: secondUserCall) - - pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin - XCTAssertNotNil(pin) - XCTAssertEqual(pin?.isLocal, true) - - try await firstUserCall.unpin(sessionId: secondUserCall.state.sessionId) - await waitForUnpinning(firstUserCall: firstUserCall, secondUserCall: secondUserCall) - - pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin - XCTAssertNil(pin) - } - - func test_joinBackstageRegularUser() async throws { - let startingDate = Date(timeIntervalSinceNow: 30) - let joiningDate = Date(timeInterval: -20, since: startingDate) - let firstUserCall = client.call(callType: .livestream, callId: randomCallId) - try await firstUserCall.create( - memberIds: [user1], - startsAt: startingDate, - backstage: .init(enabled: true, joinAheadTimeSeconds: 20) - ) - - let secondUserClient = try await makeClient(for: user2) - let secondUserCall = secondUserClient.call( - callType: .livestream, - callId: firstUserCall.callId - ) - - try await firstUserCall.join() - - let error = await XCTAssertThrowsErrorAsync { - try await secondUserCall.join() - } - let apiError = try XCTUnwrap(error as? APIError) - XCTAssertEqual(apiError.statusCode, 403) - - await fulfillment(timeout: 30) { Date() >= joiningDate } - try await secondUserCall.join() - } -} +// final class CallCRUDTests: IntegrationTest, @unchecked Sendable { +// +// let user1 = "thierry" +// let user2 = "tommaso" +// let defaultCallType = "default" +// let apiErrorCode = 16 +// let randomCallId = UUID().uuidString +// let userIdKey = MemberRequest.CodingKeys.userId.rawValue +// +// func customWait(nanoseconds duration: UInt64 = 3_000_000_000) async throws { +// try await Task.sleep(nanoseconds: duration) +// } +// +// func waitForCapability( +// _ capability: OwnCapability, +// on call: Call, +// granted: Bool = true, +// timeout: Double = 20 +// ) async -> Bool { +// let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 +// var userHasRequiredCapability = !granted +// while userHasRequiredCapability != granted && endTime > Date().timeIntervalSince1970 * 1000 { +// print("Waiting for \(capability.rawValue)") +// userHasRequiredCapability = await call.currentUserHasCapability(capability) +// } +// return userHasRequiredCapability +// } +// +// func waitForAudio( +// on call: Call, +// timeout: Double = 20 +// ) async -> Bool { +// let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 +// var usersHaveAudio = false +// while !usersHaveAudio && endTime > Date().timeIntervalSince1970 * 1000 { +// print("Waiting for Audio") +// let u1 = await call.state.participants.first!.hasAudio +// let u2 = await call.state.participants.last!.hasAudio +// usersHaveAudio = u1 && u2 +// } +// return usersHaveAudio +// } +// +// func waitForAudioLoss( +// on call: Call, +// timeout: Double = 20 +// ) async -> Bool { +// let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 +// var usersLostAudio = false +// while !usersLostAudio && endTime > Date().timeIntervalSince1970 * 1000 { +// print("Waiting for Audio Loss") +// let u1 = await call.state.participants.first!.hasAudio +// let u2 = await call.state.participants.last!.hasAudio +// usersLostAudio = u1 == false && u2 == false +// } +// return usersLostAudio +// } +// +// func waitForPinning( +// firstUserCall: Call, +// secondUserCall: Call, +// timeout: Double = 20 +// ) async { +// let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 +// var userIsPinned = false +// while !userIsPinned && endTime > Date().timeIntervalSince1970 * 1000 { +// print("Waiting for Pinning") +// let pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin +// userIsPinned = pin != nil +// } +// } +// +// func waitForUnpinning( +// firstUserCall: Call, +// secondUserCall: Call, +// timeout: Double = 20 +// ) async { +// let endTime = Date().timeIntervalSince1970 * 1000 + timeout * 1000 +// var userIsUnpinned = false +// while !userIsUnpinned && endTime > Date().timeIntervalSince1970 * 1000 { +// print("Waiting for Unpinning") +// let pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin +// userIsUnpinned = pin == nil +// } +// } +// +// func test_callCreateAndUpdate() async throws { +// let colorKey = "color" +// let red: RawJSON = "red" +// let blue: RawJSON = "blue" +// +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// +// let response = try await call.create(custom: [colorKey: red]) +// XCTAssertEqual(response.custom[colorKey], red) +// +// await assertNext(call.state.$custom) { v in +// guard let newColor = v[colorKey]?.stringValue else { +// return false +// } +// return newColor == red.stringValue +// } +// +// let updateResponse = try await call.update(custom: [colorKey: blue]) +// XCTAssertEqual(updateResponse.call.custom[colorKey], blue) +// +// await assertNext(call.state.$custom) { v in +// v[colorKey] == blue +// } +// } +// +// func test_getCallMissingId() async throws { +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// let apiErr = await XCTAssertThrowsErrorAsync { +// _ = try await call.get() +// } +// guard let apiErr = apiErr as? APIError else { +// XCTAssert((apiErr as Any) is APIError) +// return +// } +// XCTAssertEqual(apiErr.code, apiErrorCode) +// +// let expectedErrMessage = "GetCall failed with error: \"Can't find call with id \(call.cId)\"" +// XCTAssertEqual(apiErr.message, expectedErrMessage) +// } +// +// func test_getCallWrongType() async throws { +// let wrongCallType = "bananas" +// let call = client.call(callType: wrongCallType, callId: randomCallId) +// let apiErr = await XCTAssertThrowsErrorAsync { +// _ = try await call.get() +// return +// } +// guard let apiErr = apiErr as? APIError else { +// XCTAssert((apiErr as Any) is APIError) +// return +// } +// XCTAssertEqual(apiErr.code, apiErrorCode) +// +// let expectedErrMessage = "\(wrongCallType): call type does not exist" +// XCTAssertTrue(apiErr.message.localizedStandardContains(expectedErrMessage)) +// } +// +// func test_sendCustomEvent() async throws { +// let customEventKey = "test" +// let customEventValue = "asd" +// +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create() +// +// let subscription = call.subscribe(for: CustomVideoEvent.self) +// try await call.sendCustomEvent([customEventKey: .string(customEventValue)]) +// +// await assertNext(subscription) { ev in +// ev.custom[customEventKey]?.stringValue == customEventValue +// } +// } +// +// func test_createCallWithMembers() async throws { +// let roleKey = "role" +// let roleValue = "CEO" +// let membersGroup = "stars" +// let membersCount: Double = 3 +// +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1]) +// +// await assertNext(call.state.$members) { v in +// v.count == 1 && v[0].id == self.user1 +// } +// +// try await call +// .updateMembers( +// members: [ +// .init( +// custom: [ +// membersGroup: .number(membersCount) +// ], userId: user1 +// ) +// ] +// ) +// +// await fulfilmentInMainActor { +// if let member = call.state.members.first { +// return member.id == self.user1 +// && member.customData[membersGroup]?.numberValue == membersCount +// } else { +// return false +// } +// } +// +// try await call.removeMembers(ids: [user1]) +// +// await fulfilmentInMainActor { call.state.members.isEmpty } +// +// try await call.addMembers( +// members: [ +// .init( +// custom: [roleKey: .string(roleValue)], +// userId: user1 +// ) +// ] +// ) +// +// await fulfilmentInMainActor { +// if let member = call.state.members.first { +// return member.id == self.user1 +// && member.customData[roleKey]?.stringValue == roleValue +// } else { +// return false +// } +// } +// } +// +// func test_paginateCallWithMembers() async throws { +// let call1 = client.call(callType: defaultCallType, callId: randomCallId) +// try await call1.create(memberIds: [user1]) +// +// let call2 = client.call(callType: call1.callType, callId: call1.callId) +// _ = try await call2.get(membersLimit: 1) +// +// await fulfilmentInMainActor { call1.state.members.count == 1 } +// +// var membersResponse = try await call2.queryMembers() +// XCTAssertEqual(1, membersResponse.members.count) +// +// membersResponse = try await call2.queryMembers(filters: [userIdKey: .string(user1)]) +// XCTAssertEqual(1, membersResponse.members.count) +// +// membersResponse = try await call2.queryMembers(filters: [userIdKey: .string(user2)]) +// XCTAssertEqual(0, membersResponse.members.count) +// +// let secondUserClient = try await makeClient(for: user2) +// try await secondUserClient.connect() +// +// // add to call2 so we can test that the other call object is updated via WS events +// try await call2.addMembers(ids: [user2]) +// await fulfilmentInMainActor { call1.state.members.count == 2 } +// +// membersResponse = try await call2.queryMembers(filters: [userIdKey: .string(user2)]) +// XCTAssertEqual(1, membersResponse.members.count) +// +// membersResponse = try await call2.queryMembers(limit: 1) +// XCTAssertEqual(1, membersResponse.members.count) +// XCTAssertEqual(user2, membersResponse.members.first?.userId) +// +// membersResponse = try await call2.queryMembers(next: membersResponse.next) +// XCTAssertEqual(1, membersResponse.members.count) +// XCTAssertEqual(user1, membersResponse.members.first?.userId) +// +// await fulfilmentInMainActor { +// call2.state.members.count == 2 +// && call2.state.members.first?.id == self.user2 +// } +// } +// +// @MainActor +// func test_queryChannels() async throws { +// let colorKey = "color" +// let blue: RawJSON = "blue" +// +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1]) +// +// let (calls, _) = try await client.queryCalls( +// filters: [CallSortField.cid.rawValue: .string(call.cId)], +// watch: true +// ) +// XCTAssertEqual(1, calls.count) +// var fetchedCall = try XCTUnwrap(calls.first) +// XCTAssertEqual(call.cId, fetchedCall.cId) +// +// // changes to a watched call via query call should propagate as usual to the state +// let updateResponse = try await call.update(custom: [colorKey: blue]) +// XCTAssertEqual(updateResponse.call.custom[colorKey], blue) +// +// await fulfilmentInMainActor { fetchedCall.state.custom[colorKey] == blue } +// +// let (secondTry, _) = try await client.queryCalls( +// filters: [ +// CallSortField.endedAt.rawValue: .nil, +// CallSortField.cid.rawValue: .string(call.cId) +// ] +// ) +// XCTAssertEqual(1, secondTry.count) +// fetchedCall = try XCTUnwrap(secondTry.first) +// XCTAssertEqual(call.cId, fetchedCall.cId) +// +// try await call.end() +// +// let (thirdTry, _) = try await client.queryCalls( +// filters: [ +// CallSortField.endedAt.rawValue: .nil, +// CallSortField.cid.rawValue: .string(call.cId) +// ] +// ) +// XCTAssertEqual(0, thirdTry.count) +// +// await fulfilmentInMainActor { fetchedCall.state.endedAt != nil } +// } +// +// func test_sendReaction() async throws { +// let reactionType1 = "happy" +// let reactionType2 = "happyy" +// let reactionType3 = "happyyy" +// let emojiCode = ":smile:" +// let customReactionKey = "test" +// let customReactionValue = "asd" +// +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1]) +// +// let specificSub = call.subscribe(for: CallReactionEvent.self) +// +// _ = try await call.sendReaction(type: reactionType1) +// +// await assertNext(specificSub) { ev in +// ev.reaction.type == reactionType1 +// } +// +// _ = try await call.sendReaction(type: reactionType2, emojiCode: emojiCode) +// await assertNext(specificSub) { ev in +// ev.reaction.type == reactionType2 && ev.reaction.emojiCode == emojiCode +// } +// +// _ = try await call.sendReaction( +// type: reactionType3, +// custom: [customReactionKey: .string(customReactionValue)] +// ) +// await assertNext(specificSub) { ev in +// ev.reaction.type == reactionType3 && ev.reaction.custom?[customReactionKey]?.stringValue == customReactionValue +// } +// } +// +// func test_requestPermissionDiscard() async throws { +// let firstUserCall = client.call( +// callType: String.audioRoom, +// callId: randomCallId +// ) +// try await firstUserCall.create(memberIds: [user1]) +// +// let secondUserClient = try await makeClient(for: user2) +// try await secondUserClient.connect() +// let secondUserCall = secondUserClient.call( +// callType: String.audioRoom, +// callId: firstUserCall.callId +// ) +// +// _ = try await secondUserCall.get() +// var hasAudioCapability = await secondUserCall.currentUserHasCapability(.sendAudio) +// XCTAssertFalse(hasAudioCapability) +// var hasVideoCapability = await secondUserCall.currentUserHasCapability(.sendVideo) +// XCTAssertFalse(hasVideoCapability) +// +// try await secondUserCall.request(permissions: [.sendAudio]) +// +// await assertNext(firstUserCall.state.$permissionRequests) { value in +// value.count == 1 && value.first?.permission == Permission.sendAudio.rawValue +// } +// if let p = await firstUserCall.state.permissionRequests.first { +// p.reject() +// } +// +// // Test: permission requests list is now empty +// await assertNext(firstUserCall.state.$permissionRequests) { value in +// value.isEmpty +// } +// +// hasAudioCapability = await secondUserCall.currentUserHasCapability(.sendAudio) +// XCTAssertFalse(hasAudioCapability) +// hasVideoCapability = await secondUserCall.currentUserHasCapability(.sendVideo) +// XCTAssertFalse(hasVideoCapability) +// } +// +// func test_muteUserById() async throws { +// throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") +// +// let firstUserCall = client.call(callType: String.audioRoom, callId: randomCallId) +// try await firstUserCall.create(memberIds: [user1, user2]) +// try await firstUserCall.goLive() +// +// let secondUserClient = try await makeClient(for: user2) +// let secondUserCall = secondUserClient.call( +// callType: String.audioRoom, +// callId: firstUserCall.callId +// ) +// +// try await firstUserCall.join() +// try await customWait() +// +// try await firstUserCall.microphone.enable() +// try await customWait() +// +// try await secondUserCall.join() +// try await customWait() +// +// try await firstUserCall.grant(permissions: [.sendAudio], for: user2) +// try await customWait() +// +// try await secondUserCall.microphone.enable() +// try await customWait() +// +// let everyoneIsUnmutedOnTheFirstCall = await waitForAudio(on: firstUserCall) +// let everyoneIsUnmutedOnTheSecondCall = await waitForAudio(on: secondUserCall) +// XCTAssertTrue(everyoneIsUnmutedOnTheFirstCall, "Everyone should be unmuted on creator's call") +// XCTAssertTrue(everyoneIsUnmutedOnTheSecondCall, "Everyone should be unmuted on participant's call") +// +// for userId in [user1, user2] { +// try await firstUserCall.mute(userId: userId) +// } +// try await customWait() +// +// let everyoneIsMutedOnTheFirstCall = await waitForAudioLoss(on: firstUserCall) +// let everyoneIsMutedOnTheSecondCall = await waitForAudioLoss(on: secondUserCall) +// XCTAssertTrue(everyoneIsMutedOnTheFirstCall, "Everyone should be muted on creator's call") +// XCTAssertTrue(everyoneIsMutedOnTheSecondCall, "Everyone should be muted on participant's call") +// } +// +// func test_muteAllUsers() async throws { +// throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") +// +// let firstUserCall = client.call(callType: String.audioRoom, callId: randomCallId) +// try await firstUserCall.create(memberIds: [user1, user2]) +// try await firstUserCall.goLive() +// +// let secondUserClient = try await makeClient(for: user2) +// let secondUserCall = secondUserClient.call( +// callType: String.audioRoom, +// callId: firstUserCall.callId +// ) +// +// try await firstUserCall.join() +// try await customWait() +// +// try await firstUserCall.microphone.enable() +// try await customWait() +// +// try await secondUserCall.join() +// try await customWait() +// +// try await firstUserCall.grant(permissions: [.sendAudio], for: user2) +// try await customWait() +// +// try await secondUserCall.microphone.enable() +// try await customWait() +// +// let everyoneIsUnmutedOnTheFirstCall = await waitForAudio(on: firstUserCall) +// let everyoneIsUnmutedOnTheSecondCall = await waitForAudio(on: secondUserCall) +// XCTAssertTrue(everyoneIsUnmutedOnTheFirstCall, "Everyone should be unmuted on creator's call") +// XCTAssertTrue(everyoneIsUnmutedOnTheSecondCall, "Everyone should be unmuted on participant's call") +// +// try await firstUserCall.muteAllUsers() +// try await customWait() +// +// let everyoneIsMutedOnTheFirstCall = await waitForAudioLoss(on: firstUserCall) +// let everyoneIsMutedOnTheSecondCall = await waitForAudioLoss(on: secondUserCall) +// XCTAssertTrue(everyoneIsMutedOnTheFirstCall, "Everyone should be muted on creator's call") +// XCTAssertTrue(everyoneIsMutedOnTheSecondCall, "Everyone should be muted on participant's call") +// } +// +// func test_blockAndUnblockUser() async throws { +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1, user2]) +// try await call.blockUser(with: user2) +// +// var membersResponse = try await call.queryMembers() +// XCTAssertEqual(2, membersResponse.members.count) +// +// var blockedUsers = try await call.get().call.blockedUserIds +// XCTAssertEqual(blockedUsers, [user2]) +// +// try await call.unblockUser(with: user2) +// +// membersResponse = try await call.queryMembers() +// XCTAssertEqual(2, membersResponse.members.count) +// +// blockedUsers = try await call.get().call.blockedUserIds +// XCTAssertEqual(blockedUsers, []) +// } +// +// func test_createCallWithMembersAndMemberIds() async throws { +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// +// var membersRequest = [MemberRequest]() +// membersRequest.append(.init(userId: user2)) +// +// try await call.create(members: membersRequest, memberIds: [user1]) +// let membersResponse = try await call.queryMembers() +// +// XCTAssertEqual(2, membersResponse.members.count) +// } +// +// func test_grantPermissions() async throws { +// throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") +// +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1]) +// +// let expectedPermissions: [Permission] = [.sendAudio, .sendVideo, .screenshare] +// try await call.revoke(permissions: expectedPermissions, for: user1) +// +// for permission in expectedPermissions { +// let capability = try XCTUnwrap(OwnCapability(rawValue: permission.rawValue)) +// let userHasRequiredCapability = await waitForCapability(capability, on: call, granted: false) +// XCTAssertFalse(userHasRequiredCapability, "\(permission.rawValue) should not be granted") +// } +// +// try await call.grant(permissions: expectedPermissions, for: user1) +// +// for permission in expectedPermissions { +// let capability = try XCTUnwrap(OwnCapability(rawValue: permission.rawValue)) +// let userHasRequiredCapability = await waitForCapability(capability, on: call) +// XCTAssertTrue(userHasRequiredCapability, "\(permission.rawValue) permission should be granted") +// } +// } +// +// func test_grantPermissionsByRequest() async throws { +// throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") +// +// let firstUserCall = client.call(callType: String.audioRoom, callId: randomCallId) +// try await firstUserCall.create(memberIds: [user1, user2]) +// +// let secondUserClient = try await makeClient(for: user2) +// let secondUserCall = secondUserClient.call( +// callType: firstUserCall.callType, +// callId: firstUserCall.callId +// ) +// +// refreshStreamVideoProviderKey() +// +// try await firstUserCall.revoke(permissions: [.sendAudio], for: secondUserClient.user.id) +// +// var userHasUnexpectedCapability = await waitForCapability(.sendAudio, on: secondUserCall, granted: false) +// XCTAssertFalse(userHasUnexpectedCapability) +// +// try await secondUserCall.request(permissions: [.sendAudio]) +// +// userHasUnexpectedCapability = await waitForCapability(.sendAudio, on: secondUserCall, granted: false) +// XCTAssertFalse(userHasUnexpectedCapability) +// +// await assertNext(firstUserCall.state.$permissionRequests) { value in +// value.count == 1 && value.first?.permission == Permission.sendAudio.rawValue +// } +// if let p = await firstUserCall.state.permissionRequests.first { +// try await firstUserCall.grant(request: p) +// } +// +// let userHasExpectedCapability = await waitForCapability(.sendAudio, on: secondUserCall) +// XCTAssertTrue(userHasExpectedCapability) +// } +// +// func test_acceptCall() async throws { +// try await client.connect() +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1]) +// try await call.ring() +// try await customWait() +// +// var session = try await call.get().call.session +// XCTAssertEqual(session?.acceptedBy.isEmpty, true, "Call should not be accepted yet") +// +// try await call.accept() +// try await customWait() +// +// session = try await call.get().call.session +// XCTAssertNotNil(session?.acceptedBy[client.user.id], "Call should be accepted by \(user1)") +// } +// +// func test_notifyUser() async throws { +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1, user2]) +// +// let subscription = call.subscribe(for: CallNotificationEvent.self) +// +// try await call.notify() +// try await customWait() +// +// await assertNext(subscription) { [user2] ev in +// ev.members.first?.userId == user2 +// } +// } +// +// func test_setAndDeleteDevices() async throws { +// let deviceId = UUID().uuidString +// let voipDeviceId = UUID().uuidString +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1, user2]) +// +// try await call.streamVideo.setDevice(id: deviceId) +// try await call.streamVideo.setVoipDevice(id: voipDeviceId) +// try await customWait() +// var listDevices = try await call.streamVideo.listDevices() +// XCTAssertTrue(listDevices.contains(where: { $0.id == deviceId }), "Device should be added") +// XCTAssertTrue(listDevices.contains(where: { $0.id == voipDeviceId }), "Voip device should be added") +// +// try await call.streamVideo.deleteDevice(id: deviceId) +// try await call.streamVideo.deleteDevice(id: voipDeviceId) +// try await customWait() +// listDevices = try await call.streamVideo.listDevices() +// XCTAssertFalse(listDevices.contains(where: { $0.id == deviceId }), "Device should be removed") +// XCTAssertFalse(listDevices.contains(where: { $0.id == voipDeviceId }), "Voip device should be removed") +// } +// +// func test_setAndDeleteVoipDevices() async throws { +// throw XCTSkip("https://github.com/GetStream/ios-issues-tracking/issues/541") +// +// let deviceId = UUID().uuidString +// let call = client.call(callType: defaultCallType, callId: randomCallId) +// try await call.create(memberIds: [user1, user2]) +// +// try await call.streamVideo.setVoipDevice(id: deviceId) +// try await customWait() +// var listDevices = try await call.streamVideo.listDevices() +// XCTAssertTrue(listDevices.contains(where: { $0.id == deviceId })) +// +// try await call.streamVideo.deleteDevice(id: deviceId) +// try await customWait() +// listDevices = try await call.streamVideo.listDevices() +// XCTAssertFalse(listDevices.contains(where: { $0.id == deviceId })) +// } +// +// func test_pinAndUnpinUser() async throws { +// try await client.connect() +// let firstUserCall = client.call(callType: .default, callId: randomCallId) +// try await firstUserCall.create(memberIds: [user1, user2]) +// +// let secondUserClient = try await makeClient(for: user2) +// let secondUserCall = secondUserClient.call( +// callType: .default, +// callId: firstUserCall.callId +// ) +// +// try await firstUserCall.join() +// try await customWait() +// +// try await secondUserCall.join() +// try await customWait() +// +// _ = try await firstUserCall.pinForEveryone(userId: user2, sessionId: secondUserCall.state.sessionId) +// await waitForPinning(firstUserCall: firstUserCall, secondUserCall: secondUserCall) +// +// var pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin +// XCTAssertNotNil(pin) +// XCTAssertEqual(pin?.isLocal, false) +// +// _ = try await firstUserCall.unpinForEveryone(userId: user2, sessionId: secondUserCall.state.sessionId) +// await waitForUnpinning(firstUserCall: firstUserCall, secondUserCall: secondUserCall) +// +// pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin +// XCTAssertNil(pin) +// +// try await firstUserCall.pin(sessionId: secondUserCall.state.sessionId) +// await waitForPinning(firstUserCall: firstUserCall, secondUserCall: secondUserCall) +// +// pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin +// XCTAssertNotNil(pin) +// XCTAssertEqual(pin?.isLocal, true) +// +// try await firstUserCall.unpin(sessionId: secondUserCall.state.sessionId) +// await waitForUnpinning(firstUserCall: firstUserCall, secondUserCall: secondUserCall) +// +// pin = await firstUserCall.state.participantsMap[secondUserCall.state.sessionId]?.pin +// XCTAssertNil(pin) +// } +// +// func test_joinBackstageRegularUser() async throws { +// let startingDate = Date(timeIntervalSinceNow: 30) +// let joiningDate = Date(timeInterval: -20, since: startingDate) +// let firstUserCall = client.call(callType: .livestream, callId: randomCallId) +// try await firstUserCall.create( +// memberIds: [user1], +// startsAt: startingDate, +// backstage: .init(enabled: true, joinAheadTimeSeconds: 20) +// ) +// +// let secondUserClient = try await makeClient(for: user2) +// let secondUserCall = secondUserClient.call( +// callType: .livestream, +// callId: firstUserCall.callId +// ) +// +// try await firstUserCall.join() +// +// let error = await XCTAssertThrowsErrorAsync { +// try await secondUserCall.join() +// } +// let apiError = try XCTUnwrap(error as? APIError) +// XCTAssertEqual(apiError.statusCode, 403) +// +// await fulfillment(timeout: 30) { Date() >= joiningDate } +// try await secondUserCall.join() +// } +// } diff --git a/StreamVideoTests/Mock/MockInternetConnection.swift b/StreamVideoTests/Mock/MockInternetConnection.swift index af06551a4..713971876 100644 --- a/StreamVideoTests/Mock/MockInternetConnection.swift +++ b/StreamVideoTests/Mock/MockInternetConnection.swift @@ -9,6 +9,8 @@ final class MockInternetConnection: InternetConnectionProtocol, @unchecked Senda let subject: CurrentValueSubject = .init(.available(.great)) + var status: InternetConnectionStatus { subject.value } + var statusPublisher: AnyPublisher { subject.eraseToAnyPublisher() } diff --git a/StreamVideoTests/Mock/MockRTCAudioDeviceModule.swift b/StreamVideoTests/Mock/MockRTCAudioDeviceModule.swift index 2d1988699..5657f2d76 100644 --- a/StreamVideoTests/Mock/MockRTCAudioDeviceModule.swift +++ b/StreamVideoTests/Mock/MockRTCAudioDeviceModule.swift @@ -98,13 +98,41 @@ final class MockRTCAudioDeviceModule: RTCAudioDeviceModuleControlling, Mockable, stubbedFunction[function] = value } + func propertyKey( + for keyPath: KeyPath + ) -> String { + switch keyPath { + case \MockRTCAudioDeviceModule.isMicrophoneMuted: + "isMicrophoneMuted" + case \MockRTCAudioDeviceModule.isPlaying: + "isPlaying" + case \MockRTCAudioDeviceModule.isRecording: + "isRecording" + case \MockRTCAudioDeviceModule.isPlayoutInitialized: + "isPlayoutInitialized" + case \MockRTCAudioDeviceModule.isRecordingInitialized: + "isRecordingInitialized" + case \MockRTCAudioDeviceModule.isStereoPlayoutEnabled: + "isStereoPlayoutEnabled" + case \MockRTCAudioDeviceModule.isVoiceProcessingBypassed: + "isVoiceProcessingBypassed" + case \MockRTCAudioDeviceModule.isVoiceProcessingEnabled: + "isVoiceProcessingEnabled" + case \MockRTCAudioDeviceModule.isVoiceProcessingAGCEnabled: + "isVoiceProcessingAGCEnabled" + case \MockRTCAudioDeviceModule.prefersStereoPlayout: + "prefersStereoPlayout" + default: + "\(self)" + } + } + init() { stub(for: \.isMicrophoneMuted, with: false) stub(for: \.isPlaying, with: false) stub(for: \.isRecording, with: false) stub(for: \.isPlayoutInitialized, with: false) stub(for: \.isRecordingInitialized, with: false) - stub(for: \.isMicrophoneMuted, with: false) stub(for: \.isStereoPlayoutEnabled, with: false) stub(for: \.isVoiceProcessingBypassed, with: false) stub(for: \.isVoiceProcessingEnabled, with: false) diff --git a/StreamVideoTests/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule_Tests.swift b/StreamVideoTests/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule_Tests.swift index 3424afee2..d4cad8315 100644 --- a/StreamVideoTests/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule_Tests.swift +++ b/StreamVideoTests/Utils/AudioSession/AudioDeviceModule/AudioDeviceModule_Tests.swift @@ -225,7 +225,7 @@ final class AudioDeviceModule_Tests: XCTestCase, @unchecked Sendable { isPlayoutEnabled: true, isRecordingEnabled: false ) { - subject.audioDeviceModule( + _ = subject.audioDeviceModule( $0, willEnableEngine: engine, isPlayoutEnabled: true, diff --git a/StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift b/StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift index 876162af3..372a0a2fc 100644 --- a/StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift +++ b/StreamVideoTests/Utils/AudioSession/RTCAudioStore/Components/AVAudioSessionObserver_Tests.swift @@ -4,6 +4,7 @@ import AVFoundation import Combine +import StreamSwiftTestHelpers @testable import StreamVideo import XCTest @@ -40,7 +41,11 @@ final class AVAudioSessionObserver_Tests: XCTestCase, @unchecked Sendable { observer.stopObserving() } - func test_stopObserving_preventsFurtherEmissions() async { + func test_stopObserving_preventsFurtherEmissions() async throws { + try XCTSkipIf( + TestRunnerEnvironment.isCI, + "https://linear.app/stream/issue/IOS-1326/cifix-failing-test-on-ios-15-and-16-only-which-passes-locally" + ) let observer = AVAudioSessionObserver() let firstTwo = expectation(description: "first snapshots") let noMoreSnapshots = expectation(description: "no extra snapshots") diff --git a/StreamVideoTests/Utils/Proximity/Monitor/ProximityMonitor_Tests.swift b/StreamVideoTests/Utils/Proximity/Monitor/ProximityMonitor_Tests.swift index 085f38958..1d0ce82f8 100644 --- a/StreamVideoTests/Utils/Proximity/Monitor/ProximityMonitor_Tests.swift +++ b/StreamVideoTests/Utils/Proximity/Monitor/ProximityMonitor_Tests.swift @@ -76,8 +76,10 @@ final class ProximityMonitor_Tests: XCTestCase, @unchecked Sendable { function: StaticString = #function, line: UInt = #line ) async { + _ = CurrentDevice.currentValue + await wait(for: 0.5) CurrentDevice.currentValue.didUpdate(deviceType) - await fulfillment { CurrentDevice.currentValue.deviceType == deviceType } + await fulfilmentInMainActor { CurrentDevice.currentValue.deviceType == deviceType } _ = subject await subject.startObservation() diff --git a/StreamVideoTests/Utils/Store/Store_PerformanceTests.swift b/StreamVideoTests/Utils/Store/Store_PerformanceTests.swift index 2038c15d8..bdd1d1d22 100644 --- a/StreamVideoTests/Utils/Store/Store_PerformanceTests.swift +++ b/StreamVideoTests/Utils/Store/Store_PerformanceTests.swift @@ -37,7 +37,9 @@ final class Store_PerformanceTests: XCTestCase, @unchecked Sendable { func test_measureDispatchThroughput() { let iterations = 10000 - measure { + measure( + baseline: .init(1.1, stringTransformer: { String(format: "%.4fs", $0) }) + ) { for _ in 0..: CustomStringConvertible { + + var local: T + var ci: T + var stringTransformer: (T) -> String + + var description: String { + "\(stringTransformer(value)) { local:\(stringTransformer(local)), ci:\(stringTransformer(ci)) }" + } + + var value: T { + TestRunnerEnvironment.isCI ? ci : local + } + + init( + local: T, + ci: T, + stringTransformer: @escaping (T) -> String = { "\($0)" } + ) { + self.local = local + self.ci = ci + self.stringTransformer = stringTransformer + } + + init( + _ value: T, + stringTransformer: @escaping (T) -> String = { "\($0)" } + ) { + self.local = value + self.ci = value + self.stringTransformer = stringTransformer + } + } + + func measure( + baseline: ResultValue, + allowedRegression: ResultValue = .init(local: 0.15, ci: 0.25), // Default: local: 15%, ci: 25% + iterations: Int = 10, + warmup: Int = 2, + file: StaticString = #file, + function: StaticString = #function, + line: UInt = #line, + block: () -> Void + ) { + guard baseline.value > 0 else { + XCTFail("Baseline must be > 0 for \(name)", file: file, line: line) + return + } + + let samples = _measureWallClockMedian( + iterations: iterations, + warmup: warmup, + block: block + ) + + let measured = samples.sorted()[samples.endIndex / 2] // median + let diff = measured - baseline.value + let ratio = diff / baseline.value + if diff > 0, ratio > allowedRegression.value { + XCTFail( + """ + Performance regression: \(function) (\(file):\(line)) + Iterations:\(iterations), WarmUp:\(warmup) + Baseline: \(baseline) + Measured: \(String(format: "%.4f", measured))s + - Samples recorded: \(samples) + Regression: \(Int(ratio * 100))% + """, + file: file, + line: line + ) + } else { + print( + """ + Performance measurement: \(function) (\(file):\(line)) + Iterations:\(iterations), WarmUp:\(warmup) + Baseline: \(baseline) + Measured: \(String(format: "%.4f", measured))s + - Samples recorded: \(samples) + Ratio: \(Int(ratio * 100))% + """ + ) + } + } + + /// Measures wall-clock time for `iterations` runs and returns the median. + /// + /// - Important: This is intended for CI-safe regression checks. Use median to + /// reduce noise, and keep `iterations` modest to avoid long test times. + private func _measureWallClockMedian( + iterations: Int = 10, + warmup: Int = 2, + file: StaticString = #file, + line: UInt = #line, + block: () throws -> Void + ) rethrows -> [TimeInterval] { + precondition(iterations > 0) + + // Warm-up runs (not measured) + for _ in 0..