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 @@
-
-
+
+
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.'