diff --git a/.gitignore b/.gitignore index f8e6f6f2b..edfd2ef3b 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ buildServer.json # xcode-build-server files buildServer.json .compile + +# zed +.zed diff --git a/CHANGELOG.md b/CHANGELOG.md index 083aa0957..b77e290c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### 🔄 Changed +# [1.38.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.38.0) +_December 09, 2025_ + +### ✅ Added +- Improved support for moderation events handling. [#1004](https://github.com/GetStream/stream-video-swift/pull/1004) + +### 🐞 Fixed +- Pass the missing rejection reason to API calls. [#1003](https://github.com/GetStream/stream-video-swift/pull/1003) +- Mic and camera prompts showing up when not necessary. [#1005](https://github.com/GetStream/stream-video-swift/pull/1005) + # [1.37.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.37.0) _November 28, 2025_ diff --git a/DemoApp/Sources/Components/AppEnvironment.swift b/DemoApp/Sources/Components/AppEnvironment.swift index 61f4dbabf..d162feb34 100644 --- a/DemoApp/Sources/Components/AppEnvironment.swift +++ b/DemoApp/Sources/Components/AppEnvironment.swift @@ -624,6 +624,41 @@ extension AppEnvironment { }() } +extension AppEnvironment { + enum ModerationVideoPolicy: Hashable, Debuggable, Sendable { + + case blur(TimeInterval), pixelate(TimeInterval) + + var title: String { + switch self { + case let .blur(duration): + if duration > 0 { + return "Blur (\(duration)s)" + } else { + return "Blur" + } + case let .pixelate(duration): + if duration > 0 { + return "Pixelate (\(duration)s)" + } else { + return "Pixelate" + } + } + } + + var value: Moderation.VideoPolicy { + switch self { + case .blur(let duration): + return Moderation.VideoPolicy(duration: duration, videoFilter: .blur) + case .pixelate(let duration): + return Moderation.VideoPolicy(duration: duration, videoFilter: .pixelate) + } + } + } + + static var moderationVideoPolicy: ModerationVideoPolicy = .blur(20) +} + extension AppEnvironment { static var clientCapabilities: Set? diff --git a/DemoApp/Sources/Components/AppState.swift b/DemoApp/Sources/Components/AppState.swift index b62557095..b6eefffdd 100644 --- a/DemoApp/Sources/Components/AppState.swift +++ b/DemoApp/Sources/Components/AppState.swift @@ -221,6 +221,8 @@ final class AppState: ObservableObject { AppEnvironment .proximityPolicies .forEach { try? activeCall.addProximityPolicy($0.value) } + + activeCall.moderation.setVideoPolicy(AppEnvironment.moderationVideoPolicy.value) } } diff --git a/DemoApp/Sources/Components/Router.swift b/DemoApp/Sources/Components/Router.swift index 1b633464c..d816f006a 100644 --- a/DemoApp/Sources/Components/Router.swift +++ b/DemoApp/Sources/Components/Router.swift @@ -48,6 +48,13 @@ final class Router: ObservableObject { appState.unsecureRepository.save(configuration: AppEnvironment.configuration) appState.unsecureRepository.save(baseURL: AppEnvironment.baseURL) + switch AppEnvironment.baseURL { + case let .custom(_, apiKey, _): + appState.apiKey = apiKey + default: + break + } + Task { do { try await loadLoggedInUser() diff --git a/DemoApp/Sources/Components/VideoFilters/PixelateVideoFilter/ModerationPixelateVideoFilter.swift b/DemoApp/Sources/Components/VideoFilters/PixelateVideoFilter/ModerationPixelateVideoFilter.swift new file mode 100644 index 000000000..5295e2e1d --- /dev/null +++ b/DemoApp/Sources/Components/VideoFilters/PixelateVideoFilter/ModerationPixelateVideoFilter.swift @@ -0,0 +1,86 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import CoreGraphics +import CoreImage +import Foundation +import StreamVideo + +/// Applies a pixelation effect to fully obfuscate a frame. +final class ModerationPixelateVideoFilter: VideoFilter, @unchecked Sendable { + @available(*, unavailable) + override public init( + id: String, + name: String, + filter: @escaping (Input) async -> CIImage + ) { fatalError() } + + /// Creates a moderation pixelation filter. + /// - Parameters: + /// - pixelBlockFactor: Larger values create bigger pixel blocks. + /// - downscaleFactor: Downscale before pixelation to boost performance. + init( + pixelBlockFactor: CGFloat = 40, + downscaleFactor: CGFloat = 0.5 + ) { + let clampedDownscale = max(min(downscaleFactor, 1), 0.1) + let blockFactor = max(pixelBlockFactor, 1) + let name = String(describing: type(of: self)).lowercased() + + super.init( + id: "io.getstream.\(name)", + name: name, + filter: { input in + let srcImage = input.originalImage + let extent = srcImage.extent + + // Optional downscale before pixelation for better performance. + let workingImage: CIImage + if clampedDownscale < 1 { + workingImage = srcImage.transformed( + by: CGAffineTransform( + scaleX: clampedDownscale, + y: clampedDownscale + ) + ) + } else { + workingImage = srcImage + } + + let workingExtent = workingImage.extent + + let pixelate = CIFilter.pixellate() + pixelate.inputImage = workingImage + + // Big cells -> heavy censorship, size-aware. + let maxDimension = max(workingExtent.width, workingExtent.height) + pixelate.scale = Float(maxDimension / blockFactor) + + guard var out = pixelate.outputImage else { + return srcImage + } + + // If we downscaled, scale back up to original size. t + if clampedDownscale < 1 { + let scaleBack = 1 / clampedDownscale + out = out.transformed( + by: CGAffineTransform( + scaleX: scaleBack, + y: scaleBack + ) + ) + } + + // Crop to original extent to avoid any edge artifacts. + return out.cropped(to: extent) + } + ) + } +} + +extension VideoFilter { + + /// Applies a pixelation effect over the entire frame. + static let pixelate: VideoFilter = ModerationPixelateVideoFilter() +} diff --git a/DemoApp/Sources/Controls/DemoMoreControls/DemoBackgroundEffectSelector.swift b/DemoApp/Sources/Controls/DemoMoreControls/DemoBackgroundEffectSelector.swift index cb6143ce8..6a267990d 100644 --- a/DemoApp/Sources/Controls/DemoMoreControls/DemoBackgroundEffectSelector.swift +++ b/DemoApp/Sources/Controls/DemoMoreControls/DemoBackgroundEffectSelector.swift @@ -37,7 +37,11 @@ struct DemoEffectButton: View { switch effect { case .none: return appState.videoFilter == nil + case .pixelate: + return appState.videoFilter?.id == VideoFilter.pixelate.id case .blur: + return appState.videoFilter?.id == VideoFilter.blur.id + case .blurBackground: return appState.videoFilter?.id == VideoFilter.blurredBackground.id default: return appState.videoFilter?.id == effect.rawValue @@ -70,6 +74,8 @@ struct DemoEffectButton: View { enum BackgroundEffect: String, CaseIterable, Identifiable { case none case blur + case pixelate + case blurBackground case amsterdam1 = "amsterdam-1" case amsterdam2 = "amsterdam-2" case boulder1 = "boulder-1" @@ -84,7 +90,11 @@ enum BackgroundEffect: String, CaseIterable, Identifiable { switch self { case .none: return nil + case .pixelate: + return .pixelate case .blur: + return .blur + case .blurBackground: return .blurredBackground default: guard @@ -101,8 +111,12 @@ enum BackgroundEffect: String, CaseIterable, Identifiable { switch self { case .none: return Image(systemName: "circle.slash") + case .pixelate: + return Image(systemName: "square.grid.3x3.square") case .blur: return Image(systemName: "square.stack.3d.forward.dottedline.fill") + case .blurBackground: + return Image(systemName: "square.stack.3d.forward.dottedline") default: return Image(rawValue) } @@ -112,7 +126,7 @@ enum BackgroundEffect: String, CaseIterable, Identifiable { switch self { case .none: return 10 - case .blur: + case .pixelate, .blur, .blurBackground: return 10 default: return 0 diff --git a/DemoApp/Sources/Views/Login/DebugMenu.swift b/DemoApp/Sources/Views/Login/DebugMenu.swift index 9028d4e1c..ee13775d9 100644 --- a/DemoApp/Sources/Views/Login/DebugMenu.swift +++ b/DemoApp/Sources/Views/Login/DebugMenu.swift @@ -134,6 +134,13 @@ struct DebugMenu: View { didSet { AppEnvironment.clientCapabilities = preferredClientCapabilities } } + @State private var moderationVideoPolicy = AppEnvironment.moderationVideoPolicy { + didSet { AppEnvironment.moderationVideoPolicy = moderationVideoPolicy } + } + + @State private var customModerationVideoPolicyDuration: TimeInterval = 20 + @State private var presentsCustomModerationVideoPolicyDuration = false + var body: some View { Menu { makeMenu( @@ -254,6 +261,13 @@ struct DebugMenu: View { label: "Auto Leave policy" ) { self.autoLeavePolicy = $0 } + makeMenu( + for: [.blur(customModerationVideoPolicyDuration), .pixelate(customModerationVideoPolicyDuration)], + currentValue: moderationVideoPolicy, + additionalItems: { customModerationVideoView }, + label: "Moderation Video Policy" + ) { self.moderationVideoPolicy = $0 } + makeMenu( for: [.never, .twoMinutes], currentValue: disconnectionTimeout, @@ -384,6 +398,21 @@ struct DebugMenu: View { self.preferredCallType = customPreferredCallType } ) + .alertWithTextField( + title: "Enter moderation policy duration in seconds", + placeholder: "Duration", + presentationBinding: $presentsCustomModerationVideoPolicyDuration, + valueBinding: $customModerationVideoPolicyDuration, + transformer: { TimeInterval($0) ?? 0 }, + action: { + switch moderationVideoPolicy { + case .blur: + moderationVideoPolicy = .blur(customModerationVideoPolicyDuration) + case .pixelate: + moderationVideoPolicy = .pixelate(customModerationVideoPolicyDuration) + } + } + ) } @ViewBuilder @@ -402,11 +431,7 @@ struct DebugMenu: View { Button { presentsCustomEnvironmentSetup = true } label: { - Label { - Text("Custom") - } icon: { - EmptyView() - } + Text("Custom") } } } @@ -427,11 +452,7 @@ struct DebugMenu: View { Button { presentsCustomTokenExpiration = true } label: { - Label { - Text("Custom") - } icon: { - EmptyView() - } + Text("Custom") } } } @@ -452,11 +473,7 @@ struct DebugMenu: View { Button { presentsCustomCallExpiration = true } label: { - Label { - Text("Custom") - } icon: { - EmptyView() - } + Text("Custom") } } } @@ -477,11 +494,7 @@ struct DebugMenu: View { Button { presentsCustomDisconnectionTimeout = true } label: { - Label { - Text("Custom") - } icon: { - EmptyView() - } + Text("Custom") } } } @@ -499,6 +512,15 @@ struct DebugMenu: View { } } + @ViewBuilder + private var customModerationVideoView: some View { + Button { + presentsCustomModerationVideoPolicyDuration = true + } label: { + Text("Duration") + } + } + @ViewBuilder private func makeMenu( for items: [Item], diff --git a/DocumentationTests/DocumentationTests/DocumentationTests.xcodeproj/project.pbxproj b/DocumentationTests/DocumentationTests/DocumentationTests.xcodeproj/project.pbxproj index c96b3e68f..8540e7b4a 100644 --- a/DocumentationTests/DocumentationTests/DocumentationTests.xcodeproj/project.pbxproj +++ b/DocumentationTests/DocumentationTests/DocumentationTests.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 400062A42EDF390D0086E14B /* 27-moderation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400062A32EDF390D0086E14B /* 27-moderation.swift */; }; 400D91B62B63D88100EBA47D /* DocumentationTests.h in Headers */ = {isa = PBXBuildFile; fileRef = 400D91B52B63D88100EBA47D /* DocumentationTests.h */; settings = {ATTRIBUTES = (Public, ); }; }; 400D91C72B63D96800EBA47D /* 03-quickstart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400D91C62B63D96800EBA47D /* 03-quickstart.swift */; }; 400D91C92B63DB3700EBA47D /* 01-client-auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400D91C82B63DB3700EBA47D /* 01-client-auth.swift */; }; @@ -90,6 +91,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 400062A32EDF390D0086E14B /* 27-moderation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "27-moderation.swift"; sourceTree = ""; }; 400D91B22B63D88100EBA47D /* DocumentationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DocumentationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 400D91B52B63D88100EBA47D /* DocumentationTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DocumentationTests.h; sourceTree = ""; }; 400D91C62B63D96800EBA47D /* 03-quickstart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-quickstart.swift"; sourceTree = ""; }; @@ -292,6 +294,7 @@ 401C1EF32D494CED00304609 /* 24-closed-captions.swift */, 40895E612E264BB000D3049D /* 25-incoming-video-state.swift */, 4050725C2E5F416A003D2109 /* 26-permissions-prompt-customization.swift */, + 400062A32EDF390D0086E14B /* 27-moderation.swift */, ); path = "05-ui-cookbook"; sourceTree = ""; @@ -481,6 +484,7 @@ 400D91D52B63E27300EBA47D /* 07-dependency-injection.swift in Sources */, 40FFDC512B63EF58004DA7A2 /* 05-call-controls.swift in Sources */, 400D91D32B63DFA500EBA47D /* 06-querying-calls.swift in Sources */, + 400062A42EDF390D0086E14B /* 27-moderation.swift in Sources */, 401C1EF42D494CED00304609 /* 24-closed-captions.swift in Sources */, 845494D72DB9039000211413 /* 13-livestreaming.swift in Sources */, 40FFDC922B63FF70004DA7A2 /* 06-lobby-preview.swift in Sources */, diff --git a/DocumentationTests/DocumentationTests/DocumentationTests/05-ui-cookbook/27-moderation.swift b/DocumentationTests/DocumentationTests/DocumentationTests/05-ui-cookbook/27-moderation.swift new file mode 100644 index 000000000..ba19ace1f --- /dev/null +++ b/DocumentationTests/DocumentationTests/DocumentationTests/05-ui-cookbook/27-moderation.swift @@ -0,0 +1,31 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Combine +import StreamVideo +import StreamVideoSwiftUI +import SwiftUI + +@MainActor +private func content() { + container { + struct CallContainer: View { + @StateObject var viewModel: CallViewModel + + var body: some View { + Group { + // Body content + // ... + } + .moderationWarning(call: viewModel.call) + } + } + } + + container { + let call = streamVideo.call(callType: "default", callId: "my-call-id") + let videoPolicy = Moderation.VideoPolicy(duration: 10, videoFilter: .blur) + call.moderation.setVideoPolicy(videoPolicy) + } +} diff --git a/Package.swift b/Package.swift index 3759a0bc3..aafde697b 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-protobuf.git", exact: "1.30.0"), - .package(url: "https://github.com/GetStream/stream-video-swift-webrtc.git", exact: "137.0.52") + .package(url: "https://github.com/GetStream/stream-video-swift-webrtc.git", exact: "137.0.54") ], targets: [ .target( diff --git a/README.md b/README.md index 73ab41b5e..401094e96 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@

StreamVideo - StreamVideoSwiftUI - StreamVideoUIKit + StreamVideoSwiftUI + StreamVideoUIKit StreamWebRTC

diff --git a/Sources/StreamVideo/Call.swift b/Sources/StreamVideo/Call.swift index bdd94f166..ed44e255b 100644 --- a/Sources/StreamVideo/Call.swift +++ b/Sources/StreamVideo/Call.swift @@ -53,6 +53,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { self, activeCallPublisher: streamVideo.state.$activeCall.eraseToAnyPublisher() ) + /// Provides access to moderation. + public lazy var moderation = Moderation.Manager(self) private let disposableBag = DisposableBag() internal let callController: CallController @@ -123,6 +125,9 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { } _ = closedCaptionsAdapter + _ = moderation + _ = proximity + callController.call = self speaker.call = self // It's important to instantiate the stateMachine as soon as possible @@ -456,7 +461,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { stateMachine.transition( .rejecting( self, - input: .rejecting(.init(deliverySubject: deliverySubject)) + input: .rejecting(.init(reason: reason, deliverySubject: deliverySubject)) ) ) return try await deliverySubject.nextValue(timeout: CallConfiguration.timeout.reject) @@ -527,14 +532,15 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { await callController.updateTrackSize(trackSize, for: participant) } - /// Sets a `videoFilter` for the current call. - /// - Parameter videoFilter: A `VideoFilter` instance representing the video filter to set. + /// Sets a `VideoFilter` for the current call. + /// - Parameter videoFilter: Desired filter; pass `nil` to clear it. public func setVideoFilter(_ videoFilter: VideoFilter?) { + moderation.setVideoFilter(videoFilter) callController.setVideoFilter(videoFilter) } - /// Sets an`audioFilter` for the current call. - /// - Parameter audioFilter: An `AudioFilter` instance representing the audio filter to set. + /// Sets an `AudioFilter` for the current call. + /// - Parameter audioFilter: Desired filter; pass `nil` to clear it. public func setAudioFilter(_ audioFilter: AudioFilter?) { streamVideo.videoConfig.audioProcessingModule.setAudioFilter(audioFilter) } @@ -550,7 +556,12 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { try await callController.stopScreensharing() } - public func eventPublisher(for event: WSEvent.Type) -> AnyPublisher { + /// Publishes web socket events filtered by the provided type. + /// - Parameter event: Event type to observe. + /// - Returns: Publisher emitting instances of `WSEvent`. + public func eventPublisher( + for event: WSEvent.Type + ) -> AnyPublisher { eventPublisher .compactMap { $0.rawValue as? WSEvent } .eraseToAnyPublisher() diff --git a/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift b/Sources/StreamVideo/Generated/SystemEnvironment+Version.swift index f04c70441..c6fdce4bc 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.37.0" + public static let version: String = "1.38.0" /// The WebRTC version. - public static let webRTCVersion: String = "137.0.52" + public static let webRTCVersion: String = "137.0.54" } diff --git a/Sources/StreamVideo/Info.plist b/Sources/StreamVideo/Info.plist index 12e96635c..372fc32a9 100644 --- a/Sources/StreamVideo/Info.plist +++ b/Sources/StreamVideo/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.37.0 + 1.38.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/Sources/StreamVideo/Moderation/Adapters/Video/Moderation+VideoAdapter.swift b/Sources/StreamVideo/Moderation/Adapters/Video/Moderation+VideoAdapter.swift new file mode 100644 index 000000000..ee986c641 --- /dev/null +++ b/Sources/StreamVideo/Moderation/Adapters/Video/Moderation+VideoAdapter.swift @@ -0,0 +1,90 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Combine +import Foundation + +extension Moderation { + + /// Moderates outgoing video by applying policies to an active call. + final class VideoAdapter: @unchecked Sendable { + + @Atomic private(set) var isActive = false + private(set) var unmoderatedVideoFilter: VideoFilter? + private(set) var policy: VideoPolicy + + private weak var call: Call? + + private let disposableBag = DisposableBag() + private let processingQueue = OperationQueue(maxConcurrentOperationCount: 1) + private let timerCancellableKey = UUID().uuidString + + init( + _ call: Call, + policy: VideoPolicy = .init( + duration: 20, + videoFilter: .blur + ) + ) { + self.policy = policy + self.call = call + + call + .eventPublisher(for: CallModerationBlurEvent.self) + .receive(on: processingQueue) + .sink { [weak self] in self?.process($0) } + .store(in: disposableBag) + } + + // MARK: - Configuration + + /// Stores the last user-selected filter so it can be restored after + /// moderation finishes. + func didUpdateVideoFilter(_ videoFilter: VideoFilter?) { + processingQueue.addOperation { [weak self] in + if videoFilter?.id != self?.policy.videoFilter.id { + self?.unmoderatedVideoFilter = videoFilter + } + } + } + + /// Updates the moderation policy applied to future events. + func didUpdateFilterPolicy(_ policy: VideoPolicy) { + processingQueue.addOperation { [weak self] in + self?.policy = policy + } + } + + // MARK: - Private Helpers + + /// Activates moderation once the backend requests a blur event. + private func process(_ event: CallModerationBlurEvent) { + disposableBag.remove(timerCancellableKey) + call?.setVideoFilter(policy.videoFilter) + + isActive = true + + if policy.duration > 0 { + DefaultTimer + .publish(every: policy.duration) + .receive(on: processingQueue) + .sink { [weak self] _ in self?.deactivate() } + .store(in: disposableBag, key: timerCancellableKey) + } + } + + /// Deactivates moderation and restores the previous filter if needed. + private func deactivate() { + guard isActive else { + return + } + + disposableBag.remove(timerCancellableKey) + // Restore any filter we had before moderation + call?.setVideoFilter(unmoderatedVideoFilter) + + isActive = false + } + } +} diff --git a/Sources/StreamVideo/Moderation/Adapters/Video/Moderation+VideoPolicy.swift b/Sources/StreamVideo/Moderation/Adapters/Video/Moderation+VideoPolicy.swift new file mode 100644 index 000000000..92a4cd244 --- /dev/null +++ b/Sources/StreamVideo/Moderation/Adapters/Video/Moderation+VideoPolicy.swift @@ -0,0 +1,20 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation + +extension Moderation { + + /// Policy describing what filter to apply and for how long. + public struct VideoPolicy: Sendable { + var duration: TimeInterval + var videoFilter: VideoFilter + + /// Creates a policy that blurs video for a limited amount of time. + public init(duration: TimeInterval, videoFilter: VideoFilter) { + self.duration = duration + self.videoFilter = videoFilter + } + } +} diff --git a/Sources/StreamVideo/Moderation/Moderation+Manager.swift b/Sources/StreamVideo/Moderation/Moderation+Manager.swift new file mode 100644 index 000000000..a39aced2b --- /dev/null +++ b/Sources/StreamVideo/Moderation/Moderation+Manager.swift @@ -0,0 +1,30 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation + +extension Moderation { + + /// Coordinates moderation actions such as applying video filters. + public final class Manager { + + let video: VideoAdapter + + init(_ call: Call) { + self.video = .init(call) + } + + // MARK: - Interaction + + /// Stores a caller-selected filter so it can be restored post-moderation. + func setVideoFilter(_ videoFilter: VideoFilter?) { + video.didUpdateVideoFilter(videoFilter) + } + + /// Overrides the current policy used when moderation events fire. + public func setVideoPolicy(_ policy: VideoPolicy) { + video.didUpdateFilterPolicy(policy) + } + } +} diff --git a/Sources/StreamVideo/Moderation/Moderation.swift b/Sources/StreamVideo/Moderation/Moderation.swift new file mode 100644 index 000000000..b589e7429 --- /dev/null +++ b/Sources/StreamVideo/Moderation/Moderation.swift @@ -0,0 +1,7 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation + +public enum Moderation {} diff --git a/Sources/StreamVideo/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicy.swift b/Sources/StreamVideo/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicy.swift index 061808121..64e7fa1bd 100644 --- a/Sources/StreamVideo/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicy.swift +++ b/Sources/StreamVideo/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicy.swift @@ -20,6 +20,7 @@ import Foundation public struct OwnCapabilitiesAudioSessionPolicy: AudioSessionPolicy { @Injected(\.applicationStateAdapter) private var applicationStateAdapter + @Injected(\.currentDevice) private var currentDevice /// Initializes a new `OwnCapabilitiesAudioSessionPolicy` instance. public init() {} diff --git a/Sources/StreamVideo/Utils/CurrentDevice/CurrentDevice.swift b/Sources/StreamVideo/Utils/CurrentDevice/CurrentDevice.swift index 56217de96..e860edb8b 100644 --- a/Sources/StreamVideo/Utils/CurrentDevice/CurrentDevice.swift +++ b/Sources/StreamVideo/Utils/CurrentDevice/CurrentDevice.swift @@ -109,6 +109,10 @@ public final class CurrentDevice: @unchecked Sendable { self.deviceType = currentDeviceProvider() } } + + func didUpdate(_ deviceType: DeviceType) { + self.deviceType = deviceType + } } extension CurrentDevice: InjectionKey { diff --git a/Sources/StreamVideo/Utils/Logger/Logger+WebRTC.swift b/Sources/StreamVideo/Utils/Logger/Logger+WebRTC.swift index 4ff3291c1..674752353 100644 --- a/Sources/StreamVideo/Utils/Logger/Logger+WebRTC.swift +++ b/Sources/StreamVideo/Utils/Logger/Logger+WebRTC.swift @@ -8,7 +8,7 @@ import StreamWebRTC extension Logger { public enum WebRTC { - public enum LogMode { case none, validFilesOnly, all } + public enum LogMode: Sendable { case none, validFilesOnly, all } public nonisolated(unsafe) static var mode: LogMode = .all { didSet { RTCLogger.default.didUpdate(mode: mode) } @@ -69,6 +69,8 @@ extension Logger.WebRTC { } guard mode != .none else { + logger.stop() + isRunning = false return } diff --git a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/BlurBackgroundFilter/BlurBackgroundVideoFilter.swift b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/BlurBackgroundFilter/BlurBackgroundVideoFilter.swift index b9633f2b2..a541237e2 100644 --- a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/BlurBackgroundFilter/BlurBackgroundVideoFilter.swift +++ b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/BlurBackgroundFilter/BlurBackgroundVideoFilter.swift @@ -2,6 +2,7 @@ // Copyright © 2025 Stream.io Inc. All rights reserved. // +import CoreGraphics import CoreImage import Foundation @@ -15,9 +16,6 @@ import Foundation /// This filter is available on iOS 15.0 and later. @available(iOS 15.0, *) public final class BlurBackgroundVideoFilter: VideoFilter, @unchecked Sendable { - - private let backgroundImageFilterProcessor = BackgroundImageFilterProcessor() - @available(*, unavailable) override public init( id: String, @@ -25,24 +23,45 @@ public final class BlurBackgroundVideoFilter: VideoFilter, @unchecked Sendable { filter: @escaping (Input) async -> CIImage ) { fatalError() } - init() { + /// Creates a filter that applies a Gaussian blur to the background. + /// - Parameters: + /// - blurRadius: Radius used by the Gaussian blur. + /// - downscaleFactor: Downscales before blur to improve performance. + public init( + blurRadius: CGFloat = 20, + downscaleFactor: CGFloat = 0.5 + ) { + let clampedDownscale = max(min(downscaleFactor, 1), 0.1) + let radius = blurRadius + let backgroundImageFilterProcessor = BackgroundImageFilterProcessor() let name = String(describing: type(of: self)).lowercased() + super.init( id: "io.getstream.\(name)", name: name, - filter: \.originalImage + filter: { [backgroundImageFilterProcessor] input in + let srcImage = input.originalImage + let extent = srcImage.extent + let workingImage: CIImage + if clampedDownscale < 1 { + let scaleTransform = CGAffineTransform(scaleX: clampedDownscale, y: clampedDownscale) + workingImage = srcImage.transformed(by: scaleTransform) + } else { + workingImage = srcImage + } + let clampedImage = workingImage.clampedToExtent() + var blurredImage = clampedImage.applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: radius]) + if clampedDownscale < 1 { + let scaleUpTransform = CGAffineTransform(scaleX: 1 / clampedDownscale, y: 1 / clampedDownscale) + blurredImage = blurredImage.transformed(by: scaleUpTransform) + } + let backgroundImage = blurredImage.cropped(to: extent) + return backgroundImageFilterProcessor + .applyFilter( + input.originalPixelBuffer, + backgroundImage: backgroundImage + ) ?? input.originalImage + } ) - - filter = { [backgroundImageFilterProcessor] input in - let backgroundImage = input - .originalImage - .applyingFilter("CIGaussianBlur") - - return backgroundImageFilterProcessor - .applyFilter( - input.originalPixelBuffer, - backgroundImage: backgroundImage - ) ?? input.originalImage - } } } diff --git a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/ImageBackgroundFilter/ImageBackgroundVideoFilter.swift b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/ImageBackgroundFilter/ImageBackgroundVideoFilter.swift index 24e5c1404..0c2e930d1 100644 --- a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/ImageBackgroundFilter/ImageBackgroundVideoFilter.swift +++ b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/ImageBackgroundFilter/ImageBackgroundVideoFilter.swift @@ -13,23 +13,6 @@ import Foundation @available(iOS 15.0, *) public final class ImageBackgroundVideoFilter: VideoFilter, @unchecked Sendable { - private struct CacheValue: Hashable { - var originalImageSize: CGSize - var originalImageOrientation: CGImagePropertyOrientation - var result: CIImage - - func hash(into hasher: inout Hasher) { - hasher.combine(originalImageSize.width) - hasher.combine(originalImageSize.height) - hasher.combine(originalImageOrientation) - } - } - - private let backgroundImage: CIImage - private let backgroundImageFilterProcessor = BackgroundImageFilterProcessor() - - private var cachedValue: CacheValue? - @available(*, unavailable) override public init( id: String, @@ -37,55 +20,71 @@ public final class ImageBackgroundVideoFilter: VideoFilter, @unchecked Sendable filter: @escaping (Input) async -> CIImage ) { fatalError() } - /// Initializes a new `ImageBackgroundVideoFilter` instance. - /// + /// Creates a background-replacement filter that overlays a static image. /// - Parameters: - /// - backgroundImage: The `CIImage` to use as the background. - /// - id: A unique identifier for the filter. + /// - backgroundImage: Original image used as the new background. + /// - id: Unique identifier for the filter. init( _ backgroundImage: CIImage, id: String ) { let name = String(describing: type(of: self)) - self.backgroundImage = backgroundImage - - super.init(id: id, name: name, filter: \.originalImage) + let backgroundImageFilterProcessor = BackgroundImageFilterProcessor() + let cache = Cache(backgroundImage: backgroundImage) - filter = { [backgroundImageFilterProcessor, weak self] input in - guard - let backgroundImage = self?.backgroundImage(for: input) - else { - return input.originalImage + super.init( + id: id, + name: name, + filter: { [backgroundImageFilterProcessor, cache] input in + let backgroundImage = cache.backgroundImage(for: input) + return backgroundImageFilterProcessor.applyFilter( + input.originalPixelBuffer, + backgroundImage: backgroundImage + ) ?? input.originalImage } + ) + } +} - return backgroundImageFilterProcessor.applyFilter( - input.originalPixelBuffer, - backgroundImage: backgroundImage - ) ?? input.originalImage +@available(iOS 15.0, *) +extension ImageBackgroundVideoFilter { + + private final class Cache: @unchecked Sendable { + private struct Entry { + var originalImageSize: CGSize + var originalImageOrientation: CGImagePropertyOrientation + var result: CIImage } - } - /// Returns the cached or processed background image for a given input. - private func backgroundImage(for input: Input) -> CIImage { - if - let cachedValue = cachedValue, - cachedValue.originalImageSize == input.originalImage.extent.size, - cachedValue.originalImageOrientation == input.originalImageOrientation { - return cachedValue.result - } else { - var cachedBackgroundImage = backgroundImage.oriented(input.originalImageOrientation) + private var cachedValue: Entry? + private let backgroundImage: CIImage - if cachedBackgroundImage.extent.size != input.originalImage.extent.size { - cachedBackgroundImage = cachedBackgroundImage - .resize(input.originalImage.extent.size) ?? cachedBackgroundImage - } + init(backgroundImage: CIImage) { + self.backgroundImage = backgroundImage + } + + /// Returns a cached background image sized/oriented to the input frame. + func backgroundImage(for input: Input) -> CIImage { + if + let cachedValue = cachedValue, + cachedValue.originalImageSize == input.originalImage.extent.size, + cachedValue.originalImageOrientation == input.originalImageOrientation { + return cachedValue.result + } else { + var cachedBackgroundImage = backgroundImage.oriented(input.originalImageOrientation) - cachedValue = .init( - originalImageSize: input.originalImage.extent.size, - originalImageOrientation: input.originalImageOrientation, - result: cachedBackgroundImage - ) - return cachedBackgroundImage + if cachedBackgroundImage.extent.size != input.originalImage.extent.size { + cachedBackgroundImage = cachedBackgroundImage + .resize(input.originalImage.extent.size) ?? cachedBackgroundImage + } + + cachedValue = .init( + originalImageSize: input.originalImage.extent.size, + originalImageOrientation: input.originalImageOrientation, + result: cachedBackgroundImage + ) + return cachedBackgroundImage + } } } } diff --git a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/Moderation/BlurVideoFilter/ModerationBlurFilter.swift b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/Moderation/BlurVideoFilter/ModerationBlurFilter.swift new file mode 100644 index 000000000..eb502d114 --- /dev/null +++ b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/Moderation/BlurVideoFilter/ModerationBlurFilter.swift @@ -0,0 +1,79 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import CoreImage +import Foundation + +/// A video filter that applies a strong Gaussian blur to the entire frame, +/// suitable for content moderation (e.g. hiding NSFW content). +/// +public final class ModerationBlurVideoFilter: VideoFilter, @unchecked Sendable { + @available(*, unavailable) + override public init( + id: String, + name: String, + filter: @escaping (Input) async -> CIImage + ) { fatalError() } + + /// Creates a blur filter that hides the entire frame for moderation needs. + /// - Parameters: + /// - blurRadius: Radius for the Gaussian blur. + /// - downscaleFactor: Downscale applied before blur for performance. + public init( + blurRadius: CGFloat = 25, + downscaleFactor: CGFloat = 0.5 + ) { + let clampedDownscale = max(min(downscaleFactor, 1), 0.1) + let radius = blurRadius + let name = String(describing: type(of: self)).lowercased() + + super.init( + id: "io.getstream.\(name)", + name: name, + filter: { input in + let srcImage = input.originalImage + let extent = srcImage.extent + + // Optional downscale before blur for better performance. + let workingImage: CIImage + if clampedDownscale < 1 { + workingImage = srcImage.transformed( + by: CGAffineTransform( + scaleX: clampedDownscale, + y: clampedDownscale + ) + ) + } else { + workingImage = srcImage + } + + // Clamp to avoid edge transparency, then blur. + let clamped = workingImage.clampedToExtent() + + let blur = CIFilter.gaussianBlur() + blur.inputImage = clamped + blur.radius = Float(radius) + + guard var blurred = blur.outputImage else { + // Fallback: return original image if filter fails. + return srcImage + } + + // If we downscaled, scale back up to original size. + if clampedDownscale < 1 { + let scaleBack = 1 / clampedDownscale + blurred = blurred.transformed( + by: CGAffineTransform( + scaleX: scaleBack, + y: scaleBack + ) + ) + } + + // Crop to original extent to avoid any clamping artifacts. + return blurred.cropped(to: extent) + } + ) + } +} diff --git a/Sources/StreamVideo/WebRTC/VideoFilters/Utilities/BackgroundImageFilterProcessor.swift b/Sources/StreamVideo/WebRTC/VideoFilters/Utilities/BackgroundImageFilterProcessor.swift index 8a49c39d7..21c93a92a 100644 --- a/Sources/StreamVideo/WebRTC/VideoFilters/Utilities/BackgroundImageFilterProcessor.swift +++ b/Sources/StreamVideo/WebRTC/VideoFilters/Utilities/BackgroundImageFilterProcessor.swift @@ -14,16 +14,12 @@ import Vision /// background image using the mask. This allows for effects like background /// replacement or blurring. @available(iOS 15.0, *) -final class BackgroundImageFilterProcessor { +final class BackgroundImageFilterProcessor: @unchecked Sendable { private let requestHandler = VNSequenceRequestHandler() private let request: VNGeneratePersonSegmentationRequest - /// Initializes a new `BackgroundImageFilterProcessor` instance. - /// - /// - Parameters: - /// - qualityLevel: The quality level for segmentation, defaults to - /// `.balanced` if a neural engine is available, otherwise `.fast` for - /// performance. + /// Creates a processor configured with a Vision segmentation quality level. + /// - Parameter qualityLevel: Person-mask fidelity to request from Vision. init( _ qualityLevel: VNGeneratePersonSegmentationRequest.QualityLevel = neuralEngineExists ? .balanced : .fast ) { @@ -33,12 +29,11 @@ final class BackgroundImageFilterProcessor { self.request = request } - /// Applies the filter to a video frame using a background image. - /// + /// Applies the filter to a frame and blends it with a background image. /// - Parameters: - /// - buffer: The video frame to process as a `CVPixelBuffer`. - /// - backgroundImage: The background image to blend with the foreground. - /// - Returns: A new `CIImage` with the processed frame, or `nil` if an error occurs. + /// - buffer: Video frame buffer to process. + /// - backgroundImage: Background image used when compositing. + /// - Returns: Processed `CIImage` or `nil` on failure. func applyFilter( _ buffer: CVPixelBuffer, backgroundImage: CIImage diff --git a/Sources/StreamVideo/WebRTC/VideoFilters/VideoFilters.swift b/Sources/StreamVideo/WebRTC/VideoFilters/VideoFilters.swift index f9c77b8c2..4b7245e81 100644 --- a/Sources/StreamVideo/WebRTC/VideoFilters/VideoFilters.swift +++ b/Sources/StreamVideo/WebRTC/VideoFilters/VideoFilters.swift @@ -5,7 +5,7 @@ import Foundation import StreamWebRTC -open class VideoFilter: @unchecked Sendable { +open class VideoFilter: @unchecked Sendable, Equatable { /// An object which encapsulates the required input for a Video filter. public struct Input { @@ -42,6 +42,10 @@ open class VideoFilter: @unchecked Sendable { self.name = name self.filter = filter } + + public static func == (lhs: VideoFilter, rhs: VideoFilter) -> Bool { + lhs.id == rhs.id && lhs.name == rhs.name + } } extension VideoFilter { @@ -50,7 +54,7 @@ extension VideoFilter { @available(iOS 15.0, *) public static let blurredBackground: VideoFilter = BlurBackgroundVideoFilter() - /// Applies the provided image as a background on which, overlays the person in the image (video frame). + /// Replaces the background with a provided image while keeping people. @available(iOS 15.0, *) public static func imageBackground( _ backgroundImage: CIImage, @@ -58,4 +62,7 @@ extension VideoFilter { ) -> VideoFilter { ImageBackgroundVideoFilter(backgroundImage, id: id) } + + /// Applies a blur effect over the entire frame. + public static let blur: VideoFilter = ModerationBlurVideoFilter() } diff --git a/Sources/StreamVideo/WebRTC/v2/WebRTCAuthenticator.swift b/Sources/StreamVideo/WebRTC/v2/WebRTCAuthenticator.swift index e9e8a5ded..0ad54ccba 100644 --- a/Sources/StreamVideo/WebRTC/v2/WebRTCAuthenticator.swift +++ b/Sources/StreamVideo/WebRTC/v2/WebRTCAuthenticator.swift @@ -95,6 +95,15 @@ struct WebRTCAuthenticator: WebRTCAuthenticating { if audioStore.state.currentRoute.isExternal, result.speakerOn { result = result.withUpdatedSpeakerState(false) } + + if result.audioOn, !response.ownCapabilities.contains(.sendAudio) { + result = result.withUpdatedAudioState(false) + } + + if result.videoOn, !response.ownCapabilities.contains(.sendVideo) { + result = result.withUpdatedVideoState(false) + } + return result }() diff --git a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/Moderation/ModerationBlurViewModifier.swift b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/Moderation/ModerationBlurViewModifier.swift deleted file mode 100644 index c81903c06..000000000 --- a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/Moderation/ModerationBlurViewModifier.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -import StreamVideo -import SwiftUI - -/// A view modifier that blurs a participant when moderation blur toggles. -struct ModerationBlurViewModifier: ViewModifier { - - var call: Call? - var participant: CallParticipant - var blurRadius: Float - - @State var isBlurred: Bool = false - - func body(content: Content) -> some View { - Group { - if isBlurred { - content - .blur(radius: .init(blurRadius)) - } else { - content - } - } - .onReceive( - call? - .eventPublisher(for: CallModerationBlurEvent.self) - .filter { $0.userId == participant.userId } - .map { _ in !isBlurred } - .removeDuplicates() - .receive(on: DispatchQueue.main) - ) { isBlurred = $0 } - } -} - -extension View { - - /// Applies a moderation blur effect that responds to moderation events. - @ViewBuilder - public func moderationBlur( - call: Call?, - participant: CallParticipant, - blurRadius: Float = 30 - ) -> some View { - modifier( - ModerationBlurViewModifier( - call: call, - participant: participant, - blurRadius: blurRadius - ) - ) - } -} diff --git a/Sources/StreamVideoSwiftUI/Info.plist b/Sources/StreamVideoSwiftUI/Info.plist index 12e96635c..372fc32a9 100644 --- a/Sources/StreamVideoSwiftUI/Info.plist +++ b/Sources/StreamVideoSwiftUI/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.37.0 + 1.38.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/Sources/StreamVideoSwiftUI/ViewFactory.swift b/Sources/StreamVideoSwiftUI/ViewFactory.swift index b63e4b71e..2ffff101d 100644 --- a/Sources/StreamVideoSwiftUI/ViewFactory.swift +++ b/Sources/StreamVideoSwiftUI/ViewFactory.swift @@ -273,7 +273,6 @@ extension ViewFactory { customData: customData, call: call ) - .moderationBlur(call: call, participant: participant) } public func makeVideoCallParticipantModifier( diff --git a/Sources/StreamVideoUIKit/Info.plist b/Sources/StreamVideoUIKit/Info.plist index 12e96635c..372fc32a9 100644 --- a/Sources/StreamVideoUIKit/Info.plist +++ b/Sources/StreamVideoUIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.37.0 + 1.38.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/StreamVideo-XCFramework.podspec b/StreamVideo-XCFramework.podspec index c97790d0e..2c0cf8861 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.37.0' + spec.version = '1.38.0' spec.summary = 'StreamVideo iOS Video Client' spec.description = 'StreamVideo is the official Swift client for Stream Video, a service for building video applications.' @@ -24,7 +24,7 @@ Pod::Spec.new do |spec| spec.prepare_command = <<-CMD mkdir -p Frameworks/ - curl -sL "https://github.com/GetStream/stream-video-swift-webrtc/releases/download/137.0.52/StreamWebRTC.xcframework.zip" -o Frameworks/StreamWebRTC.zip + curl -sL "https://github.com/GetStream/stream-video-swift-webrtc/releases/download/137.0.54/StreamWebRTC.xcframework.zip" -o Frameworks/StreamWebRTC.zip unzip -o Frameworks/StreamWebRTC.zip -d Frameworks/ rm Frameworks/StreamWebRTC.zip CMD diff --git a/StreamVideo.podspec b/StreamVideo.podspec index 8fdd01d4d..60a14ef16 100644 --- a/StreamVideo.podspec +++ b/StreamVideo.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideo' - spec.version = '1.37.0' + spec.version = '1.38.0' spec.summary = 'StreamVideo iOS Video Client' spec.description = 'StreamVideo is the official Swift client for Stream Video, a service for building video applications.' @@ -25,7 +25,7 @@ Pod::Spec.new do |spec| spec.prepare_command = <<-CMD mkdir -p Frameworks/ - curl -sL "https://github.com/GetStream/stream-video-swift-webrtc/releases/download/137.0.52/StreamWebRTC.xcframework.zip" -o Frameworks/StreamWebRTC.zip + curl -sL "https://github.com/GetStream/stream-video-swift-webrtc/releases/download/137.0.54/StreamWebRTC.xcframework.zip" -o Frameworks/StreamWebRTC.zip unzip -o Frameworks/StreamWebRTC.zip -d Frameworks/ rm Frameworks/StreamWebRTC.zip CMD diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 8a5325bf0..567b9afae 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -8,6 +8,18 @@ /* Begin PBXBuildFile section */ 27293A6712944001B2C5E10D /* LoggerConcurrency_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD04B38CCAB544BBA6FEF29C /* LoggerConcurrency_Tests.swift */; }; + 400062792EDDF19A0086E14B /* ModerationBlurFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400062782EDDF19A0086E14B /* ModerationBlurFilter.swift */; }; + 400062902EDEFA570086E14B /* Moderation+Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4000628E2EDEFA570086E14B /* Moderation+Manager.swift */; }; + 400062912EDEFA570086E14B /* Moderation+VideoPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4000628B2EDEFA570086E14B /* Moderation+VideoPolicy.swift */; }; + 400062922EDEFA570086E14B /* Moderation+VideoAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4000628A2EDEFA570086E14B /* Moderation+VideoAdapter.swift */; }; + 400062952EDEFD2D0086E14B /* Moderation+ManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400062942EDEFD2D0086E14B /* Moderation+ManagerTests.swift */; }; + 400062992EDEFD8F0086E14B /* VideoFilter+Dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400062982EDEFD8F0086E14B /* VideoFilter+Dummy.swift */; }; + 4000629A2EDEFD8F0086E14B /* VideoFilter+Dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400062982EDEFD8F0086E14B /* VideoFilter+Dummy.swift */; }; + 4000629D2EDEFF6A0086E14B /* Moderation+VideoAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4000629C2EDEFF6A0086E14B /* Moderation+VideoAdapterTests.swift */; }; + 4000629F2EDF0D020086E14B /* ModerationPixelateVideoFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4000627C2EDDF9A40086E14B /* ModerationPixelateVideoFilter.swift */; }; + 400062A12EDF37960086E14B /* Moderation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 400062A02EDF37960086E14B /* Moderation.swift */; }; + 400062A52EDF4F0E0086E14B /* ModerationPixelateVideoFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4000627C2EDDF9A40086E14B /* ModerationPixelateVideoFilter.swift */; }; + 400062A62EDF4F0E0086E14B /* ModerationPixelateVideoFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4000627C2EDDF9A40086E14B /* ModerationPixelateVideoFilter.swift */; }; 40034C202CFDABE600A318B1 /* PublishOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C1F2CFDABE600A318B1 /* PublishOptions.swift */; }; 40034C262CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C252CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift */; }; 40034C282CFE156800A318B1 /* CallKitAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C272CFE156800A318B1 /* CallKitAvailabilityPolicy.swift */; }; @@ -616,7 +628,6 @@ 40B31AA82D10594F005FB448 /* PublishOptions+Dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B31AA72D10594F005FB448 /* PublishOptions+Dummy.swift */; }; 40B31AA92D10594F005FB448 /* PublishOptions+Dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B31AA72D10594F005FB448 /* PublishOptions+Dummy.swift */; }; 40B3E53C2DBBAF9500DE8F50 /* ProximityMonitor_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B3E53B2DBBAF9500DE8F50 /* ProximityMonitor_Tests.swift */; }; - 40B3E53E2DBBB0AB00DE8F50 /* CurrentDevice+Dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B3E53D2DBBB0AB00DE8F50 /* CurrentDevice+Dummy.swift */; }; 40B3E5402DBBB6D900DE8F50 /* MockProximityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B3E53F2DBBB6D900DE8F50 /* MockProximityMonitor.swift */; }; 40B3E5422DBBB83A00DE8F50 /* ProximityManager_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B3E5412DBBB83A00DE8F50 /* ProximityManager_Tests.swift */; }; 40B3E5442DBBB99200DE8F50 /* MockProximityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B3E5432DBBB99200DE8F50 /* MockProximityPolicy.swift */; }; @@ -672,7 +683,6 @@ 40B8FFC22EC394AA0061E3F6 /* CallModerationWarningEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B8FFBD2EC394AA0061E3F6 /* CallModerationWarningEvent.swift */; }; 40B8FFC32EC394AA0061E3F6 /* RingCallResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B8FFBF2EC394AA0061E3F6 /* RingCallResponse.swift */; }; 40B8FFC72EC394C50061E3F6 /* ModerationWarningViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B8FFC52EC394C50061E3F6 /* ModerationWarningViewModifier.swift */; }; - 40B8FFC82EC394C50061E3F6 /* ModerationBlurViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B8FFC42EC394C50061E3F6 /* ModerationBlurViewModifier.swift */; }; 40B8FFCD2EC394D30061E3F6 /* BatteryStoreDefaultReducer_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B8FFCA2EC394D30061E3F6 /* BatteryStoreDefaultReducer_Tests.swift */; }; 40B8FFCE2EC394D30061E3F6 /* BatteryStore_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B8FFC92EC394D30061E3F6 /* BatteryStore_Tests.swift */; }; 40B8FFCF2EC394D30061E3F6 /* BatteryStoreObservationMiddleware_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B8FFCB2EC394D30061E3F6 /* BatteryStoreObservationMiddleware_Tests.swift */; }; @@ -1841,6 +1851,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 400062782EDDF19A0086E14B /* ModerationBlurFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationBlurFilter.swift; sourceTree = ""; }; + 4000627C2EDDF9A40086E14B /* ModerationPixelateVideoFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationPixelateVideoFilter.swift; sourceTree = ""; }; + 4000628A2EDEFA570086E14B /* Moderation+VideoAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Moderation+VideoAdapter.swift"; sourceTree = ""; }; + 4000628B2EDEFA570086E14B /* Moderation+VideoPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Moderation+VideoPolicy.swift"; sourceTree = ""; }; + 4000628E2EDEFA570086E14B /* Moderation+Manager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Moderation+Manager.swift"; sourceTree = ""; }; + 400062942EDEFD2D0086E14B /* Moderation+ManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Moderation+ManagerTests.swift"; sourceTree = ""; }; + 400062982EDEFD8F0086E14B /* VideoFilter+Dummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VideoFilter+Dummy.swift"; sourceTree = ""; }; + 4000629C2EDEFF6A0086E14B /* Moderation+VideoAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Moderation+VideoAdapterTests.swift"; sourceTree = ""; }; + 400062A02EDF37960086E14B /* Moderation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Moderation.swift; sourceTree = ""; }; 40034C1F2CFDABE600A318B1 /* PublishOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishOptions.swift; sourceTree = ""; }; 40034C252CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitAvailabilityPolicyProtocol.swift; sourceTree = ""; }; 40034C272CFE156800A318B1 /* CallKitAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitAvailabilityPolicy.swift; sourceTree = ""; }; @@ -2299,7 +2318,6 @@ 40B284E22D52423B0064C1FE /* AVAudioSessionCategory+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAudioSessionCategory+Convenience.swift"; sourceTree = ""; }; 40B31AA72D10594F005FB448 /* PublishOptions+Dummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublishOptions+Dummy.swift"; sourceTree = ""; }; 40B3E53B2DBBAF9500DE8F50 /* ProximityMonitor_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityMonitor_Tests.swift; sourceTree = ""; }; - 40B3E53D2DBBB0AB00DE8F50 /* CurrentDevice+Dummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CurrentDevice+Dummy.swift"; sourceTree = ""; }; 40B3E53F2DBBB6D900DE8F50 /* MockProximityMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProximityMonitor.swift; sourceTree = ""; }; 40B3E5412DBBB83A00DE8F50 /* ProximityManager_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityManager_Tests.swift; sourceTree = ""; }; 40B3E5432DBBB99200DE8F50 /* MockProximityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProximityPolicy.swift; sourceTree = ""; }; @@ -2349,7 +2367,6 @@ 40B8FFBD2EC394AA0061E3F6 /* CallModerationWarningEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallModerationWarningEvent.swift; sourceTree = ""; }; 40B8FFBE2EC394AA0061E3F6 /* RingCallRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingCallRequest.swift; sourceTree = ""; }; 40B8FFBF2EC394AA0061E3F6 /* RingCallResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingCallResponse.swift; sourceTree = ""; }; - 40B8FFC42EC394C50061E3F6 /* ModerationBlurViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationBlurViewModifier.swift; sourceTree = ""; }; 40B8FFC52EC394C50061E3F6 /* ModerationWarningViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModerationWarningViewModifier.swift; sourceTree = ""; }; 40B8FFC92EC394D30061E3F6 /* BatteryStore_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryStore_Tests.swift; sourceTree = ""; }; 40B8FFCA2EC394D30061E3F6 /* BatteryStoreDefaultReducer_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryStoreDefaultReducer_Tests.swift; sourceTree = ""; }; @@ -3390,6 +3407,82 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 400062772EDDF18C0086E14B /* BlurVideoFilter */ = { + isa = PBXGroup; + children = ( + 400062782EDDF19A0086E14B /* ModerationBlurFilter.swift */, + ); + path = BlurVideoFilter; + sourceTree = ""; + }; + 4000627A2EDDF98F0086E14B /* Moderation */ = { + isa = PBXGroup; + children = ( + 400062772EDDF18C0086E14B /* BlurVideoFilter */, + ); + path = Moderation; + sourceTree = ""; + }; + 4000627B2EDDF9990086E14B /* PixelateVideoFilter */ = { + isa = PBXGroup; + children = ( + 4000627C2EDDF9A40086E14B /* ModerationPixelateVideoFilter.swift */, + ); + path = PixelateVideoFilter; + sourceTree = ""; + }; + 4000628C2EDEFA570086E14B /* Video */ = { + isa = PBXGroup; + children = ( + 4000628A2EDEFA570086E14B /* Moderation+VideoAdapter.swift */, + 4000628B2EDEFA570086E14B /* Moderation+VideoPolicy.swift */, + ); + path = Video; + sourceTree = ""; + }; + 4000628D2EDEFA570086E14B /* Adapters */ = { + isa = PBXGroup; + children = ( + 4000628C2EDEFA570086E14B /* Video */, + ); + path = Adapters; + sourceTree = ""; + }; + 4000628F2EDEFA570086E14B /* Moderation */ = { + isa = PBXGroup; + children = ( + 4000628D2EDEFA570086E14B /* Adapters */, + 4000628E2EDEFA570086E14B /* Moderation+Manager.swift */, + 400062A02EDF37960086E14B /* Moderation.swift */, + ); + path = Moderation; + sourceTree = ""; + }; + 400062932EDEFD220086E14B /* Moderation */ = { + isa = PBXGroup; + children = ( + 4000629B2EDEFF570086E14B /* Adapters */, + 400062942EDEFD2D0086E14B /* Moderation+ManagerTests.swift */, + ); + path = Moderation; + sourceTree = ""; + }; + 4000629B2EDEFF570086E14B /* Adapters */ = { + isa = PBXGroup; + children = ( + 4000629C2EDEFF6A0086E14B /* Moderation+VideoAdapterTests.swift */, + ); + path = Adapters; + sourceTree = ""; + }; + 4000629E2EDF0CB90086E14B /* VideoFilters */ = { + isa = PBXGroup; + children = ( + 4000627B2EDDF9990086E14B /* PixelateVideoFilter */, + ); + path = VideoFilters; + sourceTree = ""; + }; 40034C212CFE116200A318B1 /* AvailabilityPolicy */ = { isa = PBXGroup; children = ( @@ -3940,6 +4033,7 @@ 4030E5962A9DF48C003E8CBA /* Components */ = { isa = PBXGroup; children = ( + 4000629E2EDF0CB90086E14B /* VideoFilters */, 4014F1042D8C2EFE004E7EFD /* Gleap */, 845C098F2C0E0B6B00F725B3 /* SessionTimer */, 403EFC9D2BDBFDEE0057C248 /* Feedback */, @@ -4313,6 +4407,7 @@ 4065839E2B877BC400B4F979 /* Filters */ = { isa = PBXGroup; children = ( + 4000627A2EDDF98F0086E14B /* Moderation */, 406583842B87694B00B4F979 /* BlurBackgroundFilter */, 406583942B877A2A00B4F979 /* ImageBackgroundFilter */, ); @@ -5288,7 +5383,6 @@ 40B8FFC62EC394C50061E3F6 /* Moderation */ = { isa = PBXGroup; children = ( - 40B8FFC42EC394C50061E3F6 /* ModerationBlurViewModifier.swift */, 40B8FFC52EC394C50061E3F6 /* ModerationWarningViewModifier.swift */, ); path = Moderation; @@ -6068,6 +6162,7 @@ 40F017582BBEF0A800E89FD1 /* HLSSettingsResponse+Dummy.swift */, 40E9B3B22BCD93AE00ACF18F /* JoinCallResponse+Dummy.swift */, 40E9B3B42BCD93F500ACF18F /* Credentials+Dummy.swift */, + 400062982EDEFD8F0086E14B /* VideoFilter+Dummy.swift */, 40E9B3B62BCD941600ACF18F /* SFUResponse+Dummy.swift */, 40F017542BBEF03E00E89FD1 /* RecordSettingsResponse+Dummy.swift */, 40F017522BBEF01F00E89FD1 /* RingSettings+Dummy.swift */, @@ -7077,7 +7172,6 @@ 40FAAC8A2DDCB488007BF93A /* MockConsumableBucketItemTransformer.swift */, 40AAD1902D2EF18A00D10330 /* MockCaptureDevice.swift */, 40F1017F2D5D078800C49481 /* MockAudioSessionPolicy.swift */, - 40B3E53D2DBBB0AB00DE8F50 /* CurrentDevice+Dummy.swift */, 40D75C5B2E438633000E0438 /* AVAudioSessionRouteDescription+Dummy.swift */, 4019A2862E43565A00CE70A4 /* MockAudioSession.swift */, 4019A2882E4357B200CE70A4 /* MockRTCAudioStore.swift */, @@ -7656,6 +7750,7 @@ 84F737EE287C13AC00A363F4 /* StreamVideo */ = { isa = PBXGroup; children = ( + 4000628F2EDEFA570086E14B /* Moderation */, 84F737EF287C13AC00A363F4 /* StreamVideo.h */, 82B82F2229114001001B5FD7 /* Info.plist */, 404A81372DA3CC0C001F7FA8 /* CallConfiguration.swift */, @@ -7685,6 +7780,7 @@ 84F737F8287C13AD00A363F4 /* StreamVideoTests */ = { isa = PBXGroup; children = ( + 400062932EDEFD220086E14B /* Moderation */, 403FB14A2BFE14690047A696 /* CallStateMachine */, 40F0173C2BBEB85F00E89FD1 /* Utilities */, 40DE867A2BBEAA6900E88D8A /* CallKit */, @@ -8327,6 +8423,7 @@ buildActionMask = 2147483647; files = ( 82392D5F2993CCB300941435 /* ParticipantRobot.swift in Sources */, + 400062A62EDF4F0E0086E14B /* ModerationPixelateVideoFilter.swift in Sources */, 82C837E429A5333700CB6B0E /* CallDetailsPage.swift in Sources */, 82C837E229A532C000CB6B0E /* LoginPage.swift in Sources */, 82392D542993C9E100941435 /* StreamTestCase.swift in Sources */, @@ -8401,6 +8498,7 @@ 84093811288A90390089A35B /* DetailedCallingView.swift in Sources */, 84ED240D286C9515002A3186 /* DemoCallContainerView.swift in Sources */, 40F445B22A9DFFBB004BE3DA /* User+Demo.swift in Sources */, + 4000629F2EDF0D020086E14B /* ModerationPixelateVideoFilter.swift in Sources */, 40D946452AA5F67E00C8861B /* DemoCallingTopView.swift in Sources */, 847B47B72A260CF1000714CE /* CustomCallView.swift in Sources */, 40F445CC2A9E1FC9004BE3DA /* DemoChatViewFactory.swift in Sources */, @@ -8561,6 +8659,7 @@ 40AB35622B738C8100E465CC /* CustomCallView.swift in Sources */, 40AB35452B738C4100E465CC /* DemoMoreControlListButtonView.swift in Sources */, 4087223A2E13CD9D006A68CB /* DemoMoreThermalStateButtonView.swift in Sources */, + 400062A52EDF4F0E0086E14B /* ModerationPixelateVideoFilter.swift in Sources */, 40B713692A275F1400D1FE67 /* AppState.swift in Sources */, 409D2A532D88704D006A55EF /* DemoBroadcastMoreControlsListButtonView.swift in Sources */, 40AB353C2B738B5700E465CC /* DemoSnapshotViewModel.swift in Sources */, @@ -9019,6 +9118,7 @@ 84DC38BB29ADFCFD00946713 /* UnblockUserRequest.swift in Sources */, 4067F3082CDA32FA002E28BD /* StreamAudioSessionAdapterDelegate.swift in Sources */, 84D6494429E9AD08002CA428 /* CallIngressResponse.swift in Sources */, + 400062792EDDF19A0086E14B /* ModerationBlurFilter.swift in Sources */, 40944D172E4E352800088AF0 /* Reducer.swift in Sources */, 40BBC4C32C6373C4002AEF92 /* WebRTCStateAdapter.swift in Sources */, 403CA9B22CC7BAD6001A88C2 /* VideoLayer.swift in Sources */, @@ -9110,6 +9210,7 @@ 8490032229D308A000AD9BB4 /* AudioSettingsRequest.swift in Sources */, 40AB34B62C5D089E00B5B6B3 /* Task+Timeout.swift in Sources */, 40F101682D5A653200C49481 /* AudioSessionPolicy.swift in Sources */, + 400062A12EDF37960086E14B /* Moderation.swift in Sources */, 408721F72E127551006A68CB /* TimerPublisher.swift in Sources */, 408521E72D661CA700F012B8 /* ThermalState+Comparable.swift in Sources */, 84DC38B629ADFCFD00946713 /* QueryMembersResponse.swift in Sources */, @@ -9157,6 +9258,9 @@ 84DC38B829ADFCFD00946713 /* UpdateUserPermissionsResponse.swift in Sources */, 40EE9D402E97B3970000EA92 /* RTCAudioStore+State.swift in Sources */, 84DC38C029ADFCFD00946713 /* UserRequest.swift in Sources */, + 400062902EDEFA570086E14B /* Moderation+Manager.swift in Sources */, + 400062912EDEFA570086E14B /* Moderation+VideoPolicy.swift in Sources */, + 400062922EDEFA570086E14B /* Moderation+VideoAdapter.swift in Sources */, 84DC389629ADFCFD00946713 /* EndCallResponse.swift in Sources */, 847BE09C29DADE0100B55D21 /* Call.swift in Sources */, 848CCCEF2AB8ED8F002E83A2 /* ThumbnailsSettings.swift in Sources */, @@ -9307,6 +9411,7 @@ 84F58B7629EE92BF00010C4C /* UniqueValues.swift in Sources */, 40B48C512D14F7AE002C4EAB /* SDPParser_Tests.swift in Sources */, 40C4E83F2E65B6E300FC29BC /* MockDefaultAPIEndpoints.swift in Sources */, + 400062952EDEFD2D0086E14B /* Moderation+ManagerTests.swift in Sources */, 84F58B9529EEBA3900010C4C /* EquatableEvent.swift in Sources */, 40A0E9682B88E04D0089E8D3 /* CIImage_Resize_Tests.swift in Sources */, 40C9E4572C98B06E00802B28 /* WebRTCConfiguration_Tests.swift in Sources */, @@ -9412,7 +9517,6 @@ 40B31AA92D10594F005FB448 /* PublishOptions+Dummy.swift in Sources */, 842747F129EED88800E063AD /* InternetConnection_Tests.swift in Sources */, 40C9E4552C988CE100802B28 /* WebRTCJoinRequestFactory_Tests.swift in Sources */, - 40B3E53E2DBBB0AB00DE8F50 /* CurrentDevice+Dummy.swift in Sources */, 84F58B9329EEB53E00010C4C /* EventMiddleware_Mock.swift in Sources */, 40FAAC892DDC9F88007BF93A /* ConsumableBucket_Tests.swift in Sources */, 40B48C2E2D14D15E002C4EAB /* StreamVideoSfuModelsPublishOption_VideoLayersTests.swift in Sources */, @@ -9431,7 +9535,9 @@ 40B3E5402DBBB6D900DE8F50 /* MockProximityMonitor.swift in Sources */, 40E1C8AB2EA1561D00AC3647 /* RTCAudioStore_CoordinatorTests.swift in Sources */, 404A812E2DA3C45C001F7FA8 /* CallStateMachine_JoinedStageTests.swift in Sources */, + 4000629D2EDEFF6A0086E14B /* Moderation+VideoAdapterTests.swift in Sources */, 40F0175D2BBEF0E200E89FD1 /* BackstageSettings+Dummy.swift in Sources */, + 400062992EDEFD8F0086E14B /* VideoFilter+Dummy.swift in Sources */, 8490032F29D6D00C00AD9BB4 /* CallController_Mock.swift in Sources */, 842747F829EEEB8200E063AD /* EventNotificationCenter_Tests.swift in Sources */, 40C9E4422C943DC000802B28 /* WebRTCCoordinatorStateMachine_ErrorStageTests.swift in Sources */, @@ -9686,7 +9792,6 @@ 40FAF3D32B10F611003F8029 /* UIDevice+Convenience.swift in Sources */, 8458872A28A3F935002A81BF /* OutgoingCallView.swift in Sources */, 40B8FFC72EC394C50061E3F6 /* ModerationWarningViewModifier.swift in Sources */, - 40B8FFC82EC394C50061E3F6 /* ModerationBlurViewModifier.swift in Sources */, 40245F3A2BE26F7200FCF075 /* StatelessAudioOutputIconView.swift in Sources */, 8457CF9128BB835F00E8CF50 /* CallView.swift in Sources */, 846E4AFD29D1DDE8003733AB /* LayoutMenuView.swift in Sources */, @@ -9879,6 +9984,7 @@ 409AF6FB2DAFF7E900EE7BF6 /* PictureInPictureScreenSharingViewTests.swift in Sources */, 401D38ED2E5DB1820024C7FB /* PermissionsPromptView_Tests.swift in Sources */, 845C09892C0DFA5E00F725B3 /* LimitsSettingsResponse+Dummy.swift in Sources */, + 4000629A2EDEFD8F0086E14B /* VideoFilter+Dummy.swift in Sources */, 401BEC4F2AE1738D00EEEAC5 /* HorizontalParticipantsListView_Tests.swift in Sources */, 401480342A5423D60029166A /* AudioValuePercentageNormaliser_Tests.swift in Sources */, 8493223B29082EDB0013C029 /* StreamVideoUITestCase.swift in Sources */, @@ -11546,7 +11652,7 @@ repositoryURL = "https://github.com/GetStream/stream-video-swift-webrtc.git"; requirement = { kind = exactVersion; - version = 137.0.52; + version = 137.0.54; }; }; 40F445C32A9E1D91004BE3DA /* XCRemoteSwiftPackageReference "stream-chat-swift-test-helpers" */ = { diff --git a/StreamVideoArtifacts.json b/StreamVideoArtifacts.json index d684e8e05..abd3b4406 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"} \ 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"} \ No newline at end of file diff --git a/StreamVideoSwiftUI-XCFramework.podspec b/StreamVideoSwiftUI-XCFramework.podspec index ff2eb82ed..a9e6591f9 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.37.0' + spec.version = '1.38.0' 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 1f2656c1d..5f9732a37 100644 --- a/StreamVideoSwiftUI.podspec +++ b/StreamVideoSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoSwiftUI' - spec.version = '1.37.0' + spec.version = '1.38.0' 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/StreamVideoSwiftUITests/CallView/PermissionsPromptView_Tests.swift b/StreamVideoSwiftUITests/CallView/PermissionsPromptView_Tests.swift index 6d101f5ae..081c74155 100644 --- a/StreamVideoSwiftUITests/CallView/PermissionsPromptView_Tests.swift +++ b/StreamVideoSwiftUITests/CallView/PermissionsPromptView_Tests.swift @@ -12,6 +12,13 @@ import XCTest @MainActor final class PermissionsPromptView_Tests: StreamVideoUITestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() + + override func tearDown() async throws { + mockStreamVideo = nil + try await super.tearDown() + } + // MARK: - Rendering // MARK: iPhone diff --git a/StreamVideoSwiftUITests/CallView/ScreenSharingView_Tests.swift b/StreamVideoSwiftUITests/CallView/ScreenSharingView_Tests.swift index 61efaaa52..4e1ba9f0c 100644 --- a/StreamVideoSwiftUITests/CallView/ScreenSharingView_Tests.swift +++ b/StreamVideoSwiftUITests/CallView/ScreenSharingView_Tests.swift @@ -10,6 +10,13 @@ import XCTest final class ScreenSharingView_Tests: StreamVideoUITestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() + + override func tearDown() async throws { + mockStreamVideo = nil + try await super.tearDown() + } + @MainActor func test_screenSharingView_snapshot() async throws { let viewModel = MockCallViewModel() diff --git a/StreamVideoSwiftUITests/CallViewModel_Tests.swift b/StreamVideoSwiftUITests/CallViewModel_Tests.swift index 0ed0df4aa..e37b60ecc 100644 --- a/StreamVideoSwiftUITests/CallViewModel_Tests.swift +++ b/StreamVideoSwiftUITests/CallViewModel_Tests.swift @@ -16,7 +16,7 @@ final class CallViewModel_Tests: XCTestCase, @unchecked Sendable { private lazy var callType: String! = .default private lazy var callId: String! = UUID().uuidString private lazy var participants: [Member]! = [firstUser, secondUser] - private var streamVideo: MockStreamVideo! + private var streamVideo: MockStreamVideo! = .init() private lazy var mockCoordinatorClient: MockDefaultAPI! = .init() private lazy var mockCall: MockCall! = .init( .dummy( diff --git a/StreamVideoSwiftUITests/CallingViews/LobbyViewModel_Tests.swift b/StreamVideoSwiftUITests/CallingViews/LobbyViewModel_Tests.swift index 92582a81e..87c992ea5 100644 --- a/StreamVideoSwiftUITests/CallingViews/LobbyViewModel_Tests.swift +++ b/StreamVideoSwiftUITests/CallingViews/LobbyViewModel_Tests.swift @@ -9,18 +9,17 @@ import XCTest @MainActor final class LobbyViewModelTests: XCTestCase, @unchecked Sendable { - private lazy var mockStreamVideo: MockStreamVideo! = .init() private lazy var subject: LobbyViewModel! = .init(callType: .default, callId: .unique) override func tearDown() async throws { subject = nil - mockStreamVideo = nil try await super.tearDown() } // MARK: - Join Events Tests func test_subscribeForCallJoinUpdates_addsNewParticipant() async throws { + let mockStreamVideo: MockStreamVideo! = .init() let mockCall = MockCall() mockCall.stub( for: .get, @@ -55,6 +54,7 @@ final class LobbyViewModelTests: XCTestCase, @unchecked Sendable { // MARK: - Leave Events Tests func test_subscribeForCallLeaveUpdates_removesParticipant() async throws { + let mockStreamVideo: MockStreamVideo! = .init() let mockCall = MockCall() mockCall.stub( for: .get, @@ -87,6 +87,7 @@ final class LobbyViewModelTests: XCTestCase, @unchecked Sendable { } func test_subscribeForCallLeaveUpdates_doesNotRemoveWrongParticipant() async throws { + let mockStreamVideo: MockStreamVideo! = .init() let mockCall = MockCall() mockCall.stub( for: .get, diff --git a/StreamVideoSwiftUITests/Livestreaming/LivestreamPlayer_Tests.swift b/StreamVideoSwiftUITests/Livestreaming/LivestreamPlayer_Tests.swift index be245c677..d19fd9de0 100644 --- a/StreamVideoSwiftUITests/Livestreaming/LivestreamPlayer_Tests.swift +++ b/StreamVideoSwiftUITests/Livestreaming/LivestreamPlayer_Tests.swift @@ -12,7 +12,13 @@ final class LivestreamPlayer_Tests: StreamVideoTestCase, @unchecked Sendable { private let callId = "test" private let callType = "livestream" - + private var mockStreamVideo: MockStreamVideo! = .init() + + override func tearDown() async throws { + mockStreamVideo = nil + try await super.tearDown() + } + @MainActor func test_livestreamPlayer_snapshot() async throws { // Given diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureContentProviderTests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureContentProviderTests.swift index e83c253f5..ff45335c3 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureContentProviderTests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureContentProviderTests.swift @@ -13,6 +13,7 @@ final class PictureInPictureContentProviderTests: XCTestCase, @unchecked Sendabl private nonisolated(unsafe) static var videoConfig: VideoConfig! = .dummy() + private var mockStreamVideo: MockStreamVideo! = .init() private lazy var store: PictureInPictureStore! = .init() private lazy var mockPeerConnectionFactory: PeerConnectionFactory! = .mock() private lazy var subject: PictureInPictureContentProvider! = .init(store: store) @@ -25,6 +26,7 @@ final class PictureInPictureContentProviderTests: XCTestCase, @unchecked Sendabl override func tearDown() async throws { store = nil mockPeerConnectionFactory = nil + mockStreamVideo = nil subject = nil try await super.tearDown() } diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureContentViewTests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureContentViewTests.swift index cc40b9537..7005718d1 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureContentViewTests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureContentViewTests.swift @@ -13,8 +13,14 @@ import XCTest @MainActor final class PictureInPictureContentViewTests: StreamVideoUITestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() private lazy var targetSize: CGSize = .init(width: 400, height: 200) + override func tearDown() async throws { + mockStreamVideo = nil + try await super.tearDown() + } + func test_content_inactive() async { AssertSnapshot( await makeSubject(.inactive), diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureParticipantModifierTests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureParticipantModifierTests.swift index fa4ffdef9..0a2aa074c 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureParticipantModifierTests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureParticipantModifierTests.swift @@ -11,6 +11,13 @@ import XCTest @MainActor final class PictureInPictureParticipantModifierTests: StreamVideoUITestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() + + override func tearDown() async throws { + mockStreamVideo = nil + try await super.tearDown() + } + func test_modifier_participant_hasVideoFalse_hasAudioFalse() { AssertSnapshot( makeView(hasAudio: false, hasVideo: false), diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureStoreTests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureStoreTests.swift index ecad3a93d..e48013a3b 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureStoreTests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/PictureInPictureStoreTests.swift @@ -11,12 +11,14 @@ import XCTest @MainActor final class PictureInPictureStoreTests: XCTestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() private var subject: PictureInPictureStore! = .init() private var disposableBag: DisposableBag! = .init() override func tearDown() async throws { subject = nil disposableBag = nil + mockStreamVideo = nil try await super.tearDown() } diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureAdapterTests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureAdapterTests.swift index 13ff49f7c..c79abeafc 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureAdapterTests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureAdapterTests.swift @@ -10,8 +10,15 @@ import XCTest @available(iOS 15.0, *) final class StreamPictureInPictureAdapterTests: XCTestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() private lazy var subject: StreamPictureInPictureAdapter! = .init() + override func tearDown() async throws { + mockStreamVideo = nil + subject = nil + try await super.tearDown() + } + // MARK: - Call updated @MainActor diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureTrackStateAdapterTests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureTrackStateAdapterTests.swift index 47296d319..d7eed9b07 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureTrackStateAdapterTests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureTrackStateAdapterTests.swift @@ -11,6 +11,7 @@ import XCTest @MainActor final class PictureInPictureTrackStateAdapterTests: XCTestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() private lazy var factory: PeerConnectionFactory! = .build(audioProcessingModule: MockAudioProcessingModule.shared) private lazy var store: PictureInPictureStore! = .init() private lazy var mockCall: MockCall! = .init() @@ -39,6 +40,7 @@ final class PictureInPictureTrackStateAdapterTests: XCTestCase, @unchecked Senda factory = nil participantA = nil participantB = nil + mockStreamVideo = nil mockCall = nil try await super.tearDown() } diff --git a/StreamVideoTests/Call/Call_Tests.swift b/StreamVideoTests/Call/Call_Tests.swift index 4c62161a5..9f07ac00a 100644 --- a/StreamVideoTests/Call/Call_Tests.swift +++ b/StreamVideoTests/Call/Call_Tests.swift @@ -634,6 +634,19 @@ final class Call_Tests: StreamVideoTestCase { XCTAssertEqual(input.2.userId, userId) } + // MARK: - setVideoFilter + + func test_setVideoFilter_moderationVideoAdapterWasUpdated() async { + let mockCallController = MockCallController() + let call = MockCall(.dummy(callController: mockCallController)) + call.stub(for: \.state, with: .init()) + let mockVideoFilter = VideoFilter(id: .unique, name: .unique, filter: \.originalImage) + + call.setVideoFilter(mockVideoFilter) + + XCTAssertEqual(call.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first, mockVideoFilter) + } + // MARK: - Private helpers private func assertUpdateState( diff --git a/StreamVideoTests/CallKit/CallKitAdapterTests.swift b/StreamVideoTests/CallKit/CallKitAdapterTests.swift index cd140b927..9445ed882 100644 --- a/StreamVideoTests/CallKit/CallKitAdapterTests.swift +++ b/StreamVideoTests/CallKit/CallKitAdapterTests.swift @@ -11,18 +11,18 @@ final class CallKitAdapterTests: XCTestCase, @unchecked Sendable { private lazy var callKitService: MockCallKitService! = .init() private lazy var subject: CallKitAdapter! = .init() - override func setUp() { - super.setUp() + @MainActor + override func setUp() async throws { + try await super.setUp() InjectedValues[\.callKitPushNotificationAdapter] = callKitPushNotificationAdapter InjectedValues[\.callKitService] = callKitService - InjectedValues[\.currentDevice] = CurrentDevice(currentDeviceProvider: { .phone }) + CurrentDevice.currentValue.didUpdate(.phone) } override func tearDown() { callKitPushNotificationAdapter = nil callKitService = nil subject = nil - InjectedValues[\.currentDevice] = CurrentDevice.currentValue super.tearDown() } diff --git a/StreamVideoTests/CallKit/CallKitServiceTests.swift b/StreamVideoTests/CallKit/CallKitServiceTests.swift index 70dd3c7c8..e8f958aa3 100644 --- a/StreamVideoTests/CallKit/CallKitServiceTests.swift +++ b/StreamVideoTests/CallKit/CallKitServiceTests.swift @@ -41,6 +41,7 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { override func setUp() { super.setUp() _ = mockPermissions + _ = mockedStreamVideo InjectedValues[\.uuidFactory] = uuidFactory mockAudioStore.makeShared() mockApplicationStateAdapter.makeShared() @@ -445,6 +446,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { XCTFail() case .ring: XCTFail() + case .setVideoFilter(videoFilter: let videoFilter): + XCTFail() } } @@ -487,6 +490,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { XCTFail() case .ring: XCTFail() + case .setVideoFilter: + XCTFail() } XCTAssertEqual(call.microphone.status, .enabled) diff --git a/StreamVideoTests/CallSettings/ProximityManager_Tests.swift b/StreamVideoTests/CallSettings/ProximityManager_Tests.swift index 98ecd1f51..3a4bacf2c 100644 --- a/StreamVideoTests/CallSettings/ProximityManager_Tests.swift +++ b/StreamVideoTests/CallSettings/ProximityManager_Tests.swift @@ -10,7 +10,7 @@ import XCTest @MainActor final class ProximityManager_Tests: XCTestCase, @unchecked Sendable { - private var mockCurrentDevice: CurrentDevice! = .dummy { .phone } + private var mockedStreamVideo: MockStreamVideo! = .init() private lazy var mockProximityMonitor: MockProximityMonitor! = .init() private lazy var mockCall: MockCall! = .init(.dummy()) private lazy var mockActiveCallSubject: PassthroughSubject! = .init() @@ -19,21 +19,20 @@ final class ProximityManager_Tests: XCTestCase, @unchecked Sendable { activeCallPublisher: mockActiveCallSubject.eraseToAnyPublisher() ) + @MainActor override func setUp() async throws { try await super.setUp() - CurrentDevice.currentValue = mockCurrentDevice - await fulfillment { CurrentDevice.currentValue.deviceType == .phone } + CurrentDevice.currentValue.didUpdate(.phone) _ = mockProximityMonitor - _ = mockCurrentDevice _ = mockCall _ = subject } override func tearDown() async throws { subject = nil + mockedStreamVideo = nil mockCall = nil mockActiveCallSubject = nil - mockCurrentDevice = nil mockProximityMonitor = nil CurrentDevice.currentValue = .init() try await super.tearDown() diff --git a/StreamVideoTests/Mock/CurrentDevice+Dummy.swift b/StreamVideoTests/Mock/CurrentDevice+Dummy.swift deleted file mode 100644 index 513c01b18..000000000 --- a/StreamVideoTests/Mock/CurrentDevice+Dummy.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Copyright © 2025 Stream.io Inc. All rights reserved. -// - -import Foundation -@testable import StreamVideo - -extension CurrentDevice { - static func dummy( - currentDeviceProvider: @MainActor @escaping @Sendable () -> DeviceType - ) -> CurrentDevice { - .init(currentDeviceProvider: currentDeviceProvider) - } -} diff --git a/StreamVideoTests/Mock/MockCall.swift b/StreamVideoTests/Mock/MockCall.swift index 59bfbfc4b..609440923 100644 --- a/StreamVideoTests/Mock/MockCall.swift +++ b/StreamVideoTests/Mock/MockCall.swift @@ -2,6 +2,7 @@ // Copyright © 2025 Stream.io Inc. All rights reserved. // +import Combine import Foundation @testable import StreamVideo @@ -19,6 +20,7 @@ final class MockCall: Call, Mockable, @unchecked Sendable { case updateTrackSize case callKitActivated case ring + case setVideoFilter } enum MockCallFunctionInputKey: Payloadable { @@ -38,6 +40,8 @@ final class MockCall: Call, Mockable, @unchecked Sendable { case ring(request: RingCallRequest) + case setVideoFilter(videoFilter: VideoFilter?) + var payload: Any { switch self { case let .join(create, options, ring, notify, callSettings): @@ -54,6 +58,9 @@ final class MockCall: Call, Mockable, @unchecked Sendable { case let .ring(request): return request + + case let .setVideoFilter(videoFilter): + return videoFilter } } } @@ -68,6 +75,14 @@ final class MockCall: Call, Mockable, @unchecked Sendable { set { stub(for: \.state, with: newValue) } } + override var eventPublisher: AnyPublisher { + if containsStub(for: \.eventPublisher) { + return self[dynamicMember: \.eventPublisher] + } else { + return super.eventPublisher + } + } + @MainActor init( _ source: Call = .dummy() @@ -194,4 +209,12 @@ final class MockCall: Call, Mockable, @unchecked Sendable { return RingCallResponse(duration: "0", membersIds: request.membersIds ?? []) } } + + override func setVideoFilter(_ videoFilter: VideoFilter?) { + stubbedFunctionInput[.setVideoFilter]?.append( + .setVideoFilter( + videoFilter: videoFilter + ) + ) + } } diff --git a/StreamVideoTests/Moderation/Adapters/Moderation+VideoAdapterTests.swift b/StreamVideoTests/Moderation/Adapters/Moderation+VideoAdapterTests.swift new file mode 100644 index 000000000..208d8edf6 --- /dev/null +++ b/StreamVideoTests/Moderation/Adapters/Moderation+VideoAdapterTests.swift @@ -0,0 +1,104 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Combine +import Foundation +@testable import StreamVideo +import XCTest + +@MainActor +final class Moderation_VideoAdapterTests: XCTestCase, @unchecked Sendable { + private var mockedStreamVideo: MockStreamVideo! = MockStreamVideo() + private lazy var call: MockCall! = .init() + private lazy var subject: Moderation.VideoAdapter! = .init(call) + + override func tearDown() async throws { + subject = nil + call = nil + mockedStreamVideo = nil + try await super.tearDown() + } + + // MARK: - init + + func test_init_hasCorrectInitialState() { + XCTAssertEqual(subject.policy.duration, 20) + XCTAssertEqual(subject.policy.videoFilter, .blur) + } + + // MARK: - didUpdateFilterPolicy + + func test_didUpdateFilterPolicy_wasUpdatedCorrectly() async { + subject.didUpdateFilterPolicy(.init(duration: 11, videoFilter: .dummy(id: "stream-test"))) + + await fulfilmentInMainActor { + self.subject.policy.videoFilter.id == "stream-test" + && self.subject.policy.duration == 11 + } + } + + // MARK: - didUpdateVideoFilter + + func test_didUpdateVideoFilter_wasUpdatedCorrectly() async { + subject.didUpdateVideoFilter(.dummy(id: "stream-test")) + + await fulfilmentInMainActor { + self.subject.unmoderatedVideoFilter?.id == "stream-test" + } + } + + // MARK: - didReceive CallModerationBlurEvent + + func test_didReceiveCallModerationBlurEvent_callSetVideoFilterCorrectly() async { + let eventSubject = PassthroughSubject() + call.stub(for: \.eventPublisher, with: eventSubject.eraseToAnyPublisher()) + _ = subject + + eventSubject.send( + .typeCallModerationBlurEvent( + CallModerationBlurEvent( + callCid: .unique, + createdAt: .distantPast, + custom: [:], + userId: "1" + ) + ) + ) + + await fulfilmentInMainActor { + self.subject.isActive == true + && self.call.timesCalled(.setVideoFilter) == 1 + && self.call.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.first == self.subject.policy.videoFilter + } + } + + func test_didReceiveCallModerationBlurEvent_withDuration_afterDurationEndsModerationVideoFilterDeactivates() async { + let eventSubject = PassthroughSubject() + call.stub(for: \.eventPublisher, with: eventSubject.eraseToAnyPublisher()) + _ = subject + subject.didUpdateFilterPolicy(.init(duration: 2, videoFilter: .dummy(id: "during"))) + subject.didUpdateVideoFilter(.dummy(id: "before")) + + eventSubject.send( + .typeCallModerationBlurEvent( + CallModerationBlurEvent( + callCid: .unique, + createdAt: .distantPast, + custom: [:], + userId: "1" + ) + ) + ) + + await fulfilmentInMainActor { + self.subject.isActive == true + } + + await fulfilmentInMainActor { + self.subject.isActive == false + && self.call.timesCalled(.setVideoFilter) == 2 + && self.call.recordedInputPayload(VideoFilter.self, for: .setVideoFilter)?.last?.id == "before" + } + } +} diff --git a/StreamVideoTests/Moderation/Moderation+ManagerTests.swift b/StreamVideoTests/Moderation/Moderation+ManagerTests.swift new file mode 100644 index 000000000..5f43d28b0 --- /dev/null +++ b/StreamVideoTests/Moderation/Moderation+ManagerTests.swift @@ -0,0 +1,40 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import Foundation +@testable import StreamVideo +import XCTest + +@MainActor +final class Moderation_ManagerTests: XCTestCase, @unchecked Sendable { + private var mockedStreamVideo: MockStreamVideo! = MockStreamVideo() + private lazy var subject: Moderation.Manager! = .init(MockCall()) + + override func tearDown() async throws { + subject = nil + mockedStreamVideo = nil + try await super.tearDown() + } + + // MARK: - setVideoFilter + + func test_setVideoFilter_videoAdapterWasUpdated() async { + subject.setVideoFilter(.dummy(id: "stream-test")) + + await fulfilmentInMainActor { + self.subject.video.unmoderatedVideoFilter?.id == "stream-test" + } + } + + // MARK: - setVideoPolicy + + func test_setVideoPolicy_videoAdapterWasUpdated() async { + subject.setVideoPolicy(.init(duration: 11, videoFilter: .dummy(id: "stream-test"))) + + await fulfilmentInMainActor { + self.subject.video.policy.videoFilter.id == "stream-test" + && self.subject.video.policy.duration == 11 + } + } +} diff --git a/StreamVideoTests/TestUtils/Mockable.swift b/StreamVideoTests/TestUtils/Mockable.swift index 4c0be8ee4..78631f738 100644 --- a/StreamVideoTests/TestUtils/Mockable.swift +++ b/StreamVideoTests/TestUtils/Mockable.swift @@ -53,6 +53,10 @@ extension Mockable { mutating func resetRecords(for key: FunctionKey) { stubbedFunctionInput[key] = [] } + + func containsStub(for keyPath: KeyPath) -> Bool { + stubbedProperty[propertyKey(for: keyPath)] != nil + } } final class StubVariantResultProvider { diff --git a/StreamVideoTests/Utilities/Dummy/VideoFilter+Dummy.swift b/StreamVideoTests/Utilities/Dummy/VideoFilter+Dummy.swift new file mode 100644 index 000000000..e62b8eaa1 --- /dev/null +++ b/StreamVideoTests/Utilities/Dummy/VideoFilter+Dummy.swift @@ -0,0 +1,22 @@ +// +// Copyright © 2025 Stream.io Inc. All rights reserved. +// + +import CoreImage +import Foundation +import StreamVideo + +extension VideoFilter { + + static func dummy( + id: String = .unique, + name: String = .unique, + filter: @escaping (Input) async -> CIImage = \.originalImage + ) -> VideoFilter { + .init( + id: id, + name: name, + filter: filter + ) + } +} diff --git a/StreamVideoTests/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicyTests.swift b/StreamVideoTests/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicyTests.swift index 2d3c452f8..bf6fd888d 100644 --- a/StreamVideoTests/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicyTests.swift +++ b/StreamVideoTests/Utils/AudioSession/Policies/OwnCapabilitiesAudioSessionPolicyTests.swift @@ -10,21 +10,16 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda private lazy var stubbedAppStateAdapter: MockAppStateAdapter! = .init() private lazy var subject: OwnCapabilitiesAudioSessionPolicy! = .init() - private lazy var currentDeviceType: CurrentDevice.DeviceType! = CurrentDevice.DeviceType.phone - private lazy var currentDevice: CurrentDevice! = .init { self.currentDeviceType } override func setUp() { super.setUp() AppStateProviderKey.currentValue = stubbedAppStateAdapter - InjectedValues[\.currentDevice] = currentDevice _ = subject } override func tearDown() { subject = nil stubbedAppStateAdapter = nil - currentDevice = nil - InjectedValues[\.currentDevice] = CurrentDevice.currentValue super.tearDown() } @@ -32,8 +27,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenUserCannotSendAudio_ReturnsPlaybackConfiguration() async { // Given - currentDeviceType = .phone - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.phone) let callSettings = CallSettings(audioOn: true, videoOn: true, speakerOn: true) let ownCapabilities: Set = [.sendVideo] @@ -54,8 +48,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenUserCanSendAudioAndAudioOn_ReturnsPlayAndRecordConfiguration() async { // Given - currentDeviceType = .phone - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.phone) let callSettings = CallSettings(audioOn: true, videoOn: true, speakerOn: false) let ownCapabilities: Set = [.sendAudio, .sendVideo] @@ -80,8 +73,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenUserCanSendAudioAndSpeakerOnWithEarpiece_ReturnsPlayAndRecordConfiguration() async { // Given - currentDeviceType = .phone - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.phone) let callSettings = CallSettings(audioOn: false, videoOn: true, speakerOn: true) let ownCapabilities: Set = [.sendAudio, .sendVideo] @@ -106,8 +98,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenUserCanSendAudioAndSpeakerOnWithoutEarpiece_ReturnsPlaybackAndRecordConfiguration() async { // Given - currentDeviceType = .pad - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.pad) let callSettings = CallSettings(audioOn: false, videoOn: true, speakerOn: true) let ownCapabilities: Set = [.sendAudio, .sendVideo] @@ -126,8 +117,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenUserCanSendAudioAndAudioOff_ReturnsPlaybackConfiguration() async { // Given - currentDeviceType = .phone - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.phone) let callSettings = CallSettings(audioOn: false, videoOn: true, speakerOn: false) let ownCapabilities: Set = [.sendAudio, .sendVideo] @@ -148,8 +138,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenVideoOffSpeakerOnBackgroundFalse_ReturnsVoiceChatMode() async { // Given - currentDeviceType = .phone - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.phone) let callSettings = CallSettings(audioOn: true, videoOn: false, speakerOn: true) let ownCapabilities: Set = [.sendAudio, .sendVideo] @@ -172,8 +161,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenVideoOffSpeakerFalseBackgroundFalse_ReturnsVoiceChatMode() async { // Given - currentDeviceType = .phone - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.phone) let callSettings = CallSettings(audioOn: true, videoOn: false, speakerOn: false) let ownCapabilities: Set = [.sendAudio, .sendVideo] @@ -196,8 +184,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenVideoOffSpeakerOnBackgroundTrue_ReturnsVoiceChatMode() async { // Given - currentDeviceType = .phone - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.phone) stubbedAppStateAdapter.stubbedState = .background let callSettings = CallSettings(audioOn: true, videoOn: false, speakerOn: true) let ownCapabilities: Set = [.sendAudio, .sendVideo] @@ -221,8 +208,7 @@ final class OwnCapabilitiesAudioSessionPolicyTests: XCTestCase, @unchecked Senda func testConfiguration_WhenVideoOffSpeakerFalseBackgroundTrue_ReturnsVoiceChatMode() async { // Given - currentDeviceType = .phone - await fulfilmentInMainActor { self.currentDevice.deviceType == self.currentDeviceType } + CurrentDevice.currentValue.didUpdate(.phone) stubbedAppStateAdapter.stubbedState = .background let callSettings = CallSettings(audioOn: true, videoOn: false, speakerOn: false) let ownCapabilities: Set = [.sendAudio, .sendVideo] diff --git a/StreamVideoTests/Utils/AudioSession/RTCAudioStore/Namespace/Effects/RTCAudioStore_StereoPlayoutEffectTests.swift b/StreamVideoTests/Utils/AudioSession/RTCAudioStore/Namespace/Effects/RTCAudioStore_StereoPlayoutEffectTests.swift index 5536c8e20..19085bb34 100644 --- a/StreamVideoTests/Utils/AudioSession/RTCAudioStore/Namespace/Effects/RTCAudioStore_StereoPlayoutEffectTests.swift +++ b/StreamVideoTests/Utils/AudioSession/RTCAudioStore/Namespace/Effects/RTCAudioStore_StereoPlayoutEffectTests.swift @@ -13,13 +13,15 @@ final class RTCAudioStore_StereoPlayoutEffectTests: XCTestCase, @unchecked Senda func test_stereoPlayoutChanges_dispatchesStereoAction() async throws { let expectation = self.expectation(description: "Expected action dispatched.") let subject = RTCAudioStore.StereoPlayoutEffect() - - let mockDispatcher = MockStoreDispatcher() - subject.dispatcher = .init { actions, _, _, _ in mockDispatcher.handle(actions: actions) } - let mockAudioDeviceModule = MockRTCAudioDeviceModule() let audioDeviceModule = AudioDeviceModule(mockAudioDeviceModule) let stateSubject = CurrentValueSubject(.dummy(audioDeviceModule: audioDeviceModule)) + subject.set(statePublisher: stateSubject.eraseToAnyPublisher()) + // We wait for the configuration on the effect to be completed. + await wait(for: 0.5) + + let mockDispatcher = MockStoreDispatcher() + subject.dispatcher = .init { actions, _, _, _ in mockDispatcher.handle(actions: actions) } let cancellable = mockDispatcher .publisher @@ -36,7 +38,6 @@ final class RTCAudioStore_StereoPlayoutEffectTests: XCTestCase, @unchecked Senda } .sink { _ in expectation.fulfill() } - subject.set(statePublisher: stateSubject.eraseToAnyPublisher()) audioDeviceModule.audioDeviceModule( .init(), didUpdateAudioProcessingState: RTCAudioProcessingState( diff --git a/StreamVideoTests/Utils/Proximity/Monitor/ProximityMonitor_Tests.swift b/StreamVideoTests/Utils/Proximity/Monitor/ProximityMonitor_Tests.swift index 464fd471d..085f38958 100644 --- a/StreamVideoTests/Utils/Proximity/Monitor/ProximityMonitor_Tests.swift +++ b/StreamVideoTests/Utils/Proximity/Monitor/ProximityMonitor_Tests.swift @@ -56,7 +56,7 @@ final class ProximityMonitor_Tests: XCTestCase, @unchecked Sendable { @MainActor func test_stopObservation_isActiveBecomesFalse() async { - CurrentDevice.currentValue = .dummy { .phone } + CurrentDevice.currentValue.didUpdate(.phone) await fulfillment { CurrentDevice.currentValue.deviceType == .phone } subject.startObservation() @@ -76,7 +76,7 @@ final class ProximityMonitor_Tests: XCTestCase, @unchecked Sendable { function: StaticString = #function, line: UInt = #line ) async { - CurrentDevice.currentValue = .dummy { deviceType } + CurrentDevice.currentValue.didUpdate(deviceType) await fulfillment { CurrentDevice.currentValue.deviceType == deviceType } _ = subject diff --git a/StreamVideoTests/Utils/Proximity/Policies/SpeakerProximityPolicy_Tests.swift b/StreamVideoTests/Utils/Proximity/Policies/SpeakerProximityPolicy_Tests.swift index 1137419b7..be55c5739 100644 --- a/StreamVideoTests/Utils/Proximity/Policies/SpeakerProximityPolicy_Tests.swift +++ b/StreamVideoTests/Utils/Proximity/Policies/SpeakerProximityPolicy_Tests.swift @@ -10,6 +10,7 @@ import XCTest @MainActor final class SpeakerProximityPolicy_Tests: XCTestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() private lazy var mockCall: MockCall! = .init(.dummy()) private lazy var peerConnectionFactory: PeerConnectionFactory! = .mock() private lazy var subject: SpeakerProximityPolicy! = .init() @@ -22,6 +23,7 @@ final class SpeakerProximityPolicy_Tests: XCTestCase, @unchecked Sendable { override func tearDown() async throws { subject = nil + mockStreamVideo = nil mockCall = nil peerConnectionFactory = nil try await super.tearDown() diff --git a/StreamVideoTests/Utils/Proximity/Policies/VideoProximityPolicy_Tests.swift b/StreamVideoTests/Utils/Proximity/Policies/VideoProximityPolicy_Tests.swift index ee3d28370..710cc286e 100644 --- a/StreamVideoTests/Utils/Proximity/Policies/VideoProximityPolicy_Tests.swift +++ b/StreamVideoTests/Utils/Proximity/Policies/VideoProximityPolicy_Tests.swift @@ -9,6 +9,7 @@ import XCTest @MainActor final class VideoProximityPolicy_Tests: XCTestCase, @unchecked Sendable { + private var mockStreamVideo: MockStreamVideo! = .init() private lazy var mockCallController: MockCallController! = .init() private lazy var mockCall: MockCall! = .init(.dummy(callController: mockCallController)) private lazy var subject: VideoProximityPolicy! = .init() @@ -24,6 +25,7 @@ final class VideoProximityPolicy_Tests: XCTestCase, @unchecked Sendable { override func tearDown() async throws { subject = nil mockCall = nil + mockStreamVideo = nil mockCallController = nil try await super.tearDown() } diff --git a/StreamVideoTests/WebRTC/v2/WebRTCAuthenticator_Tests.swift b/StreamVideoTests/WebRTC/v2/WebRTCAuthenticator_Tests.swift index 196ea715f..6da0a332b 100644 --- a/StreamVideoTests/WebRTC/v2/WebRTCAuthenticator_Tests.swift +++ b/StreamVideoTests/WebRTC/v2/WebRTCAuthenticator_Tests.swift @@ -107,117 +107,532 @@ final class WebRTCAuthenticator_Tests: XCTestCase, @unchecked Sendable { XCTAssertEqual(sfuAdapter.connectURL.absoluteString, "wss://getstream.io") } - func test_authenticate_withCreateTrueAndInitialCallSettings_shouldSetInitialCallSettings() async throws { - let create = true - let ring = true - let notify = true - let options = CreateCallOptions() - let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false)))) - mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) - let initialCallSettings = CallSettings( - audioOn: true, - videoOn: true, - speakerOn: true + // MARK: with sendAudio and sendVideo capabilities + + func test_authenticate_withCreateTrueAndInitialCallSettings_withSendAudioAndVideoCapabilities_shouldSetInitialCallSettings( + ) async throws { + try await assertCallSettings( + initialCallSettings: CallSettings(audioOn: true, videoOn: true, speakerOn: true), + result: .success( + .dummy( + call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false))), + ownCapabilities: [.sendAudio, .sendVideo] + ) + ), + expected: CallSettings(audioOn: true, videoOn: true, speakerOn: true) ) - await mockCoordinatorStack - .coordinator - .stateAdapter - .set(initialCallSettings: initialCallSettings) + } - _ = try await subject.authenticate( - coordinator: mockCoordinatorStack.coordinator, - currentSFU: nil, - create: create, - ring: ring, - notify: notify, - options: options + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendAudioAndVideoCapabilities_videoOnSpeakerFalse_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:true speaker will default to true. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: true) + ) + ), + ownCapabilities: [.sendAudio, .sendVideo] + ) + ), + expected: .init(audioOn: true, videoOn: true, speakerOn: true) ) + } - let callSettings = await mockCoordinatorStack - .coordinator - .stateAdapter - .callSettings - XCTAssertEqual(callSettings, initialCallSettings) + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendAudioAndVideoCapabilities_videoOffSpeakerTrue_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: true), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendAudio, .sendVideo] + ) + ), + expected: .init(audioOn: true, videoOn: false, speakerOn: true) + ) } - func test_authenticate_withCreateTrueWithoutInitialCallSettings_shouldSetCallSettingsFromResponse() async throws { - let create = true - let ring = true - let notify = true - let options = CreateCallOptions() - let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: true)))) - mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendAudioAndVideoCapabilities_videoOffSpeakerFalseDefaultDeviceSpeaker_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(defaultDevice: .speaker, micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendAudio, .sendVideo] + ) + ), + expected: .init(audioOn: true, videoOn: false, speakerOn: true) + ) + } - _ = try await subject.authenticate( - coordinator: mockCoordinatorStack.coordinator, - currentSFU: nil, - create: create, - ring: ring, - notify: notify, - options: options + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendAudioAndVideoCapabilities_videoOffSpeakerFalseDefaultDeviceNonSpeakershouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(defaultDevice: .earpiece, micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendAudio, .sendVideo] + ) + ), + expected: .init(audioOn: true, videoOn: false, speakerOn: false) ) + } - await fulfillment { - let callSettings = await self.mockCoordinatorStack.coordinator.stateAdapter.callSettings - return callSettings == .init(expected.call.settings) - } + func test_authenticate_withCreateFalseAndInitialCallSettings_withSendAudioAndVideoCapabilities_shouldSetInitialCallSettings( + ) async throws { + try await assertCallSettings( + create: false, + initialCallSettings: CallSettings(audioOn: true, videoOn: true, speakerOn: true), + result: .success( + .dummy( + call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false))), + ownCapabilities: [.sendAudio, .sendVideo] + ) + ), + expected: CallSettings(audioOn: true, videoOn: true, speakerOn: true) + ) } - func test_authenticate_withCreateFalseAndInitialCallSettings_shouldSetInitialCallSettings() async throws { - let create = true - let ring = true - let notify = true - let options = CreateCallOptions() - let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false)))) - mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) - let initialCallSettings = CallSettings( - audioOn: false, - videoOn: true, - speakerOn: true + func test_authenticate_withCreateFalseWithoutInitialCallSettings_withSendAudioAndVideoCapabilities_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + create: false, + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:true speaker will default to true. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: true) + ) + ), + ownCapabilities: [.sendAudio, .sendVideo] + ) + ), + expected: .init(audioOn: true, videoOn: true, speakerOn: true) ) - await mockCoordinatorStack - .coordinator - .stateAdapter - .set(initialCallSettings: initialCallSettings) + } - _ = try await subject.authenticate( - coordinator: mockCoordinatorStack.coordinator, - currentSFU: nil, - create: create, - ring: ring, - notify: notify, - options: options + // MARK: with sendAudio capability + + func test_authenticate_withCreateTrueAndInitialCallSettings_withSendAudioCapability_shouldSetInitialCallSettings() async throws { + try await assertCallSettings( + initialCallSettings: CallSettings(audioOn: true, videoOn: true, speakerOn: true), + result: .success( + .dummy( + call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false))), + ownCapabilities: [.sendAudio] + ) + ), + expected: CallSettings(audioOn: true, videoOn: false, speakerOn: true) ) + } - await fulfillment { - let callSettings = await self.mockCoordinatorStack.coordinator.stateAdapter.callSettings - return callSettings == initialCallSettings - } + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendAudioCapability_videoOnSpeakerFalse_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:true speaker will default to true. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: true) + ) + ), + ownCapabilities: [.sendAudio] + ) + ), + expected: .init(audioOn: true, videoOn: false, speakerOn: true) + ) } - func test_authenticate_withCreateFalseWithoutInitialCallSettings_shouldSetCallSettingsFromResponse() async throws { - let create = false - let ring = true - let notify = true - let options = CreateCallOptions() - let expected = JoinCallResponse.dummy(call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: true)))) - mockCoordinatorStack.callAuthenticator.authenticateResult = .success(expected) + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendAudioCapability_videoOffSpeakerTrue_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: true), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendAudio] + ) + ), + expected: .init(audioOn: true, videoOn: false, speakerOn: true) + ) + } - _ = try await subject.authenticate( - coordinator: mockCoordinatorStack.coordinator, - currentSFU: nil, - create: create, - ring: ring, - notify: notify, - options: options + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendAudioCapability_videoOffSpeakerFalseDefaultDeviceSpeaker_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(defaultDevice: .speaker, micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendAudio] + ) + ), + expected: .init(audioOn: true, videoOn: false, speakerOn: true) ) + } - await fulfillment { - let callSettings = await self.mockCoordinatorStack.coordinator.stateAdapter.callSettings - return callSettings == .init(expected.call.settings) - } + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendAudioCapability_videoOffSpeakerFalseDefaultDeviceNonSpeakershouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(defaultDevice: .earpiece, micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendAudio] + ) + ), + expected: .init(audioOn: true, videoOn: false, speakerOn: false) + ) } + func test_authenticate_withCreateFalseAndInitialCallSettings_withSendAudioCapability_shouldSetInitialCallSettings( + ) async throws { + try await assertCallSettings( + create: false, + initialCallSettings: CallSettings(audioOn: true, videoOn: true, speakerOn: true), + result: .success( + .dummy( + call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false))), + ownCapabilities: [.sendAudio] + ) + ), + expected: CallSettings(audioOn: true, videoOn: false, speakerOn: true) + ) + } + + func test_authenticate_withCreateFalseWithoutInitialCallSettings_withSendAudioCapability_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + create: false, + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:true speaker will default to true. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: true) + ) + ), + ownCapabilities: [.sendAudio] + ) + ), + expected: .init(audioOn: true, videoOn: false, speakerOn: true) + ) + } + + // MARK: with sendVideo capability + + func test_authenticate_withCreateTrueAndInitialCallSettings_withSendVideoCapability_shouldSetInitialCallSettings() async throws { + try await assertCallSettings( + initialCallSettings: CallSettings(audioOn: true, videoOn: true, speakerOn: true), + result: .success( + .dummy( + call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false))), + ownCapabilities: [.sendVideo] + ) + ), + expected: CallSettings(audioOn: false, videoOn: true, speakerOn: true) + ) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendVideoCapability_videoOnSpeakerFalse_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:true speaker will default to true. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: true) + ) + ), + ownCapabilities: [.sendVideo] + ) + ), + expected: .init(audioOn: false, videoOn: true, speakerOn: true) + ) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendVideoCapability_videoOffSpeakerTrue_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: true), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendVideo] + ) + ), + expected: .init(audioOn: false, videoOn: false, speakerOn: true) + ) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendVideoCapability_videoOffSpeakerFalseDefaultDeviceSpeaker_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(defaultDevice: .speaker, micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendVideo] + ) + ), + expected: .init(audioOn: false, videoOn: false, speakerOn: true) + ) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withSendVideoCapability_videoOffSpeakerFalseDefaultDeviceNonSpeakershouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(defaultDevice: .earpiece, micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [.sendVideo] + ) + ), + expected: .init(audioOn: false, videoOn: false, speakerOn: false) + ) + } + + func test_authenticate_withCreateFalseAndInitialCallSettings_withSendVideoCapability_shouldSetInitialCallSettings( + ) async throws { + try await assertCallSettings( + create: false, + initialCallSettings: CallSettings(audioOn: true, videoOn: true, speakerOn: true), + result: .success( + .dummy( + call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false))), + ownCapabilities: [.sendVideo] + ) + ), + expected: CallSettings(audioOn: false, videoOn: true, speakerOn: true) + ) + } + + func test_authenticate_withCreateFalseWithoutInitialCallSettings_withSendVideoCapability_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + create: false, + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:true speaker will default to true. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: true) + ) + ), + ownCapabilities: [.sendVideo] + ) + ), + expected: .init(audioOn: false, videoOn: true, speakerOn: true) + ) + } + + // MARK: without audio or video capabilities + + func test_authenticate_withCreateTrueAndInitialCallSettings_withoutCapabilities_shouldSetInitialCallSettings() async throws { + try await assertCallSettings( + initialCallSettings: CallSettings(audioOn: true, videoOn: true, speakerOn: true), + result: .success( + .dummy( + call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false))), + ownCapabilities: [] + ) + ), + expected: CallSettings(audioOn: false, videoOn: false, speakerOn: true) + ) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withoutCapabilities_videoOnSpeakerFalse_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:true speaker will default to true. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: true) + ) + ), + ownCapabilities: [] + ) + ), + expected: .init(audioOn: false, videoOn: false, speakerOn: true) + ) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withoutCapabilities_videoOffSpeakerTrue_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: true), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [] + ) + ), + expected: .init(audioOn: false, videoOn: false, speakerOn: true) + ) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withoutCapabilities_videoOffSpeakerFalseDefaultDeviceSpeaker_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(defaultDevice: .speaker, micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [] + ) + ), + expected: .init(audioOn: false, videoOn: false, speakerOn: true) + ) + } + + func test_authenticate_withCreateTrueWithoutInitialCallSettings_withoutCapabilities_videoOffSpeakerFalseDefaultDeviceNonSpeakershouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:false we respect the value of speakerDefaultOn. + audio: .dummy(defaultDevice: .earpiece, micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: false) + ) + ), + ownCapabilities: [] + ) + ), + expected: .init(audioOn: false, videoOn: false, speakerOn: false) + ) + } + + func test_authenticate_withCreateFalseAndInitialCallSettings_withoutCapabilities_shouldSetInitialCallSettings() async throws { + try await assertCallSettings( + create: false, + initialCallSettings: CallSettings(audioOn: true, videoOn: true, speakerOn: true), + result: .success( + .dummy( + call: .dummy(settings: .dummy(audio: .dummy(micDefaultOn: false))), + ownCapabilities: [] + ) + ), + expected: CallSettings(audioOn: false, videoOn: false, speakerOn: true) + ) + } + + func test_authenticate_withCreateFalseWithoutInitialCallSettings_withoutCapabilities_shouldSetCallSettingsFromResponse( + ) async throws { + try await assertCallSettings( + create: false, + initialCallSettings: nil, + result: .success( + .dummy( + call: .dummy( + settings: .dummy( + // Because videoOn:true speaker will default to true. + audio: .dummy(micDefaultOn: true, speakerDefaultOn: false), + video: .dummy(cameraDefaultOn: true) + ) + ), + ownCapabilities: [] + ) + ), + expected: .init(audioOn: false, videoOn: false, speakerOn: true) + ) + } + + // MARK: - + func test_authenticate_updatesVideoOptions() async throws { let create = false let ring = true @@ -277,6 +692,8 @@ final class WebRTCAuthenticator_Tests: XCTestCase, @unchecked Sendable { XCTAssertEqual(statsReporter?.deliveryInterval, 12) } + // MARK: with only sendAudio capability + // MARK: - waitForAuthentication func test_waitForAuthentication_shouldThrowErrorIfTimeout() async throws { @@ -332,4 +749,43 @@ final class WebRTCAuthenticator_Tests: XCTestCase, @unchecked Sendable { try await group.waitForAll() } } + + // MARK: - Private Helpers + + private func assertCallSettings( + create: Bool = true, + ring: Bool = true, + notify: Bool = true, + options: CreateCallOptions = .init(), + initialCallSettings: CallSettings? = nil, + result: Result, + expected: CallSettings, + file: StaticString = #file, + function: StaticString = #function, + line: UInt = #line + ) async throws { + mockCoordinatorStack.callAuthenticator.authenticateResult = result + + if let initialCallSettings { + await mockCoordinatorStack + .coordinator + .stateAdapter + .set(initialCallSettings: initialCallSettings) + } + + _ = try await subject.authenticate( + coordinator: mockCoordinatorStack.coordinator, + currentSFU: nil, + create: create, + ring: ring, + notify: notify, + options: options + ) + + let callSettings = await mockCoordinatorStack + .coordinator + .stateAdapter + .callSettings + XCTAssertEqual(callSettings, expected, file: file, line: line) + } } diff --git a/StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift b/StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift index ebdf19670..ab9be5403 100644 --- a/StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift +++ b/StreamVideoTests/WebRTC/v2/WebRTCCoorindator_Tests.swift @@ -294,6 +294,7 @@ final class WebRTCCoordinator_Tests: XCTestCase, @unchecked Sendable { ) try await subject.startScreensharing(type: .inApp) + await fulfillment { mockPublisher.timesCalled(.beginScreenSharing) == 1 } let actual = try XCTUnwrap( mockPublisher.recordedInputPayload( @@ -316,6 +317,7 @@ final class WebRTCCoordinator_Tests: XCTestCase, @unchecked Sendable { ) try await subject.startScreensharing(type: .broadcast) + await fulfillment { mockPublisher.timesCalled(.beginScreenSharing) == 1 } let actual = try XCTUnwrap( mockPublisher.recordedInputPayload( @@ -339,7 +341,9 @@ final class WebRTCCoordinator_Tests: XCTestCase, @unchecked Sendable { try await subject.stopScreensharing() - XCTAssertEqual(mockPublisher.timesCalled(.stopScreenSharing), 1) + await fulfillment { + mockPublisher.timesCalled(.stopScreenSharing) == 1 + } } // MARK: - changePinState diff --git a/StreamVideoUIKit-XCFramework.podspec b/StreamVideoUIKit-XCFramework.podspec index 4130a5b85..cc738ac75 100644 --- a/StreamVideoUIKit-XCFramework.podspec +++ b/StreamVideoUIKit-XCFramework.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoUIKit-XCFramework' - spec.version = '1.37.0' + spec.version = '1.38.0' spec.summary = 'StreamVideo UIKit Video Components' spec.description = 'StreamVideoUIKit SDK offers flexible UIKit components able to display data provided by StreamVideo SDK.' diff --git a/StreamVideoUIKit.podspec b/StreamVideoUIKit.podspec index ae6ed7b92..de657d2b7 100644 --- a/StreamVideoUIKit.podspec +++ b/StreamVideoUIKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'StreamVideoUIKit' - spec.version = '1.37.0' + spec.version = '1.38.0' spec.summary = 'StreamVideo UIKit Video Components' spec.description = 'StreamVideoUIKit SDK offers flexible UIKit components able to display data provided by StreamVideo SDK.'