From ea93b87229adb3b57897edb1c42dce05183eb97b Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 3 Jul 2024 15:24:40 +0200 Subject: [PATCH 01/21] Fixed some crashes --- .../Sources/Components/AppEnvironment.swift | 28 ++++---- .../Components/Chat/DemoChatAdapter.swift | 2 +- .../Components/Chat/DemoChatViewFactory.swift | 1 + .../Chat/DemoChatViewModel+Injection.swift | 2 +- .../Components/CodeScanner/CodeScanner.swift | 8 +-- .../Deeplinks/DeeplinkAdapter.swift | 2 +- .../MemoryLogDestination/LogQueue.swift | 2 +- DemoApp/Sources/Components/Router.swift | 6 +- .../SessionTimer/SessionTimer.swift | 10 ++- .../LocalParticipantSnapshotViewModel.swift | 2 +- .../Components/Snapshot/SnapshotTrigger.swift | 2 +- .../DemoMoreControls/DemoReactionButton.swift | 2 +- .../Sources/Extensions/DemoApp+Sentry.swift | 2 +- .../ViewModifiers/DemoChatModifier.swift | 2 +- .../DemoMoreControlsViewModifier.swift | 2 +- DemoApp/Sources/Views/Login/AddUserView.swift | 2 +- DemoApp/Sources/Views/Login/DebugMenu.swift | 2 + .../Views/StatsView/DemoStatsView.swift | 4 +- DemoAppUIKit/Sources/CallViewHelper.swift | 1 + Sources/StreamVideo/Call.swift | 4 +- .../StreamVideo/CallKit/CallKitAdapter.swift | 2 +- .../CallKitPushNotificationAdapter.swift | 4 +- .../StreamVideo/CallKit/CallKitService.swift | 6 +- .../Controllers/CallController.swift | 6 +- .../DependencyInjection/InjectedValues.swift | 2 +- Sources/StreamVideo/Errors/Errors.swift | 14 ++-- .../StreamVideo/HTTPClient/HTTPConfig.swift | 2 +- .../HTTPClient/InternetConnection.swift | 2 +- Sources/StreamVideo/Models/CallsQuery.swift | 2 +- .../Models/CoordinatorModels.swift | 3 +- Sources/StreamVideo/Models/Token.swift | 2 +- .../OpenApi/generated/CodableHelper.swift | 12 ++-- Sources/StreamVideo/StreamVideo.swift | 12 ++-- .../StreamVideo/StreamVideoEnvironment.swift | 4 +- .../StreamActiveCallProvider.swift | 2 +- .../StreamCallAudioRecorder.swift | 2 +- .../Utils/CallCache/CallCache.swift | 2 +- .../Destination/BaseLogDestination.swift | 2 +- .../Destination/ConsoleLogDestination.swift | 2 +- .../Logger/Destination/LogDestination.swift | 6 +- Sources/StreamVideo/Utils/Logger/Logger.swift | 34 ++++----- .../LastParticipantAutoLeavePolicy.swift | 2 +- Sources/StreamVideo/Utils/RawJSON.swift | 2 +- .../RejectionReasonProvider.swift | 2 +- Sources/StreamVideo/Utils/Sorting.swift | 26 +++---- ...treamCallStateMachine+AcceptingStage.swift | 2 +- .../Stages/StreamCallStateMachine+Stage.swift | 2 +- .../Utils/StreamRuntimeCheck.swift | 2 +- .../SystemEnvironment+XStreamClient.swift | 16 +---- .../Utils/ThermalStateObserver.swift | 4 +- Sources/StreamVideo/Utils/Timers.swift | 2 +- .../UUIDProviding/StreamUUIDFactory.swift | 2 +- Sources/StreamVideo/Utils/Utils.swift | 2 +- .../StreamAudioProcessingModule.swift | 2 +- Sources/StreamVideo/WebRTC/AudioSession.swift | 8 ++- .../WebRTC/DefaultRTCMediaConstraints.swift | 4 +- .../StreamVideo/WebRTC/PeerConnection.swift | 4 +- Sources/StreamVideo/WebRTC/Retries.swift | 2 +- .../BroadcastBufferConnection.swift | 2 +- .../Screensharing/BroadcastBufferReader.swift | 2 + .../BroadcastBufferUploader.swift | 2 +- .../BroadcastSampleHandler.swift | 2 +- .../StreamVideo/WebRTC/SfuMiddleware.swift | 2 +- .../WebRTC/SimulatorScreenCapturer.swift | 6 +- .../WebRTC/StreamVideoCaptureHandler.swift | 26 ++++--- .../StreamVideo/WebRTC/VideoCapturer.swift | 6 +- Sources/StreamVideo/WebRTC/WebRTCClient.swift | 26 +++---- .../Client/BackgroundTaskScheduler.swift | 72 ++++++++++--------- .../Client/ConnectionRecoveryHandler.swift | 24 ++++--- .../WebSockets/Client/ConnectionStatus.swift | 2 +- .../WebSockets/Client/RetryStrategy.swift | 4 +- .../Client/URLSessionWebSocketEngine.swift | 13 ++-- .../WebSockets/Client/WebSocketClient.swift | 8 +-- .../WebSockets/Client/WebSocketEngine.swift | 2 +- .../WebSockets/Events/EventBatcher.swift | 16 ++--- .../Events/EventNotificationCenter.swift | 9 ++- .../WebSockets/Events/JsonEventDecoder.swift | 4 +- .../WebSockets/Events/StreamJsonDecoder.swift | 8 +-- .../protobuf/sfu/event/events.pb.swift | 8 +-- .../protobuf/sfu/models/models.pb.swift | 20 +++--- Sources/StreamVideoSwiftUI/Appearance.swift | 6 +- .../CallParticipantMenuAction.swift | 2 +- .../CallView/ParticipantsGridLayout.swift | 2 +- .../ScreenSharing/ScreenSharingView.swift | 2 +- .../CallView/VideoRenderer.swift | 12 ++-- .../ViewModifiers/CallEndedViewModifier.swift | 5 +- .../ParticipantEventViewModifier.swift | 2 +- .../Snapshot/SnapshotViewModifier.swift | 2 +- .../iOS13/BackportStateObject.swift | 10 +-- Sources/StreamVideoSwiftUI/Utils.swift | 2 +- Sources/StreamVideoSwiftUI/Utils/Camera.swift | 23 +++--- .../StreamDeviceOrientationAdapter.swift | 48 +++++++------ .../Utils/Formatters/Formatters.swift | 2 +- .../Utils/HelperViews.swift | 2 +- .../Utils/KeyboardReadable.swift | 24 +------ ...tureInPictureVideoCallViewController.swift | 4 +- .../StreamPictureInPictureAdapter.swift | 5 +- .../StreamPictureInPictureController.swift | 4 +- .../StreamYUVToARGBConversion.swift | 2 +- .../StreamPixelBufferRepository.swift | 2 +- .../StreamVideoSwiftUI/Utils/ToastView.swift | 3 +- .../VideoRendererPool/VideoRendererPool.swift | 6 +- .../StreamVideoUIKit/Utils/Animation.swift | 4 +- .../Utils/UIView+Extensions.swift | 3 + StreamVideo.xcodeproj/project.pbxproj | 30 ++++---- 105 files changed, 400 insertions(+), 364 deletions(-) diff --git a/DemoApp/Sources/Components/AppEnvironment.swift b/DemoApp/Sources/Components/AppEnvironment.swift index ac148e560..56a124214 100644 --- a/DemoApp/Sources/Components/AppEnvironment.swift +++ b/DemoApp/Sources/Components/AppEnvironment.swift @@ -21,7 +21,7 @@ extension AppEnvironment { var isTest: Bool { self == .test } } - static var configuration: Configuration = { + static let configuration: Configuration = { #if STREAM_RELEASE return .release #elseif STREAM_E2E_TESTS @@ -34,7 +34,7 @@ extension AppEnvironment { extension AppEnvironment { - indirect enum BaseURL: Debuggable, CaseIterable { + indirect enum BaseURL: Debuggable, CaseIterable, Sendable { case pronto case prontoStaging case staging @@ -99,7 +99,7 @@ extension AppEnvironment { } } - static var allCases: [BaseURL] = [ + static let allCases: [BaseURL] = [ .pronto, .prontoStaging, .staging, @@ -108,7 +108,7 @@ extension AppEnvironment { ] } - static var baseURL: BaseURL = { + nonisolated(unsafe) static var baseURL: BaseURL = { switch configuration { case .test: return .demo @@ -126,7 +126,7 @@ extension AppEnvironment { case universal = "streamvideo" } - static var appURLScheme: String = { AppURLScheme.universal.rawValue }() + static let appURLScheme: String = { AppURLScheme.universal.rawValue }() } extension AppEnvironment { @@ -137,7 +137,7 @@ extension AppEnvironment { var url: URL { URL(string: rawValue)! } } - static var authBaseURL: URL = { AuthBaseURL.universal.url }() + static let authBaseURL: URL = { AuthBaseURL.universal.url }() } extension AppEnvironment { @@ -192,7 +192,7 @@ extension AppEnvironment { } } - static var loggedInView: LoggedInView = { + nonisolated(unsafe) static var loggedInView: LoggedInView = { switch configuration { case .test: return .detailed @@ -223,7 +223,7 @@ extension AppEnvironment { } } - static var performanceTrackerVisibility: PerformanceTrackerVisibility = { + nonisolated(unsafe) static var performanceTrackerVisibility: PerformanceTrackerVisibility = { .hidden }() } @@ -243,7 +243,7 @@ extension AppEnvironment { } } - static var chatIntegration: ChatIntegration = { + nonisolated(unsafe) static var chatIntegration: ChatIntegration = { .enabled }() } @@ -283,7 +283,7 @@ extension AppEnvironment { } } - static var supportedDeeplinks: [SupportedDeeplink] = { + nonisolated(unsafe) static var supportedDeeplinks: [SupportedDeeplink] = { switch configuration { case .debug: return [.pronto, .demo, .staging, .legacy] @@ -310,7 +310,7 @@ extension AppEnvironment { } } - static var pictureInPictureIntegration: PictureInPictureIntegration = { + nonisolated(unsafe) static var pictureInPictureIntegration: PictureInPictureIntegration = { .enabled }() } @@ -360,7 +360,7 @@ extension AppEnvironment { } } - static var tokenExpiration: TokenExpiration = { + nonisolated(unsafe) static var tokenExpiration: TokenExpiration = { switch configuration { case .debug: return .oneMinute @@ -412,7 +412,7 @@ extension AppEnvironment { } } - static var callExpiration: CallExpiration = .never + nonisolated(unsafe) static var callExpiration: CallExpiration = .never } extension AppEnvironment { @@ -440,5 +440,5 @@ extension AppEnvironment { } } - static var autoLeavePolicy: AutoLeavePolicy = .default + nonisolated(unsafe) static var autoLeavePolicy: AutoLeavePolicy = .default } diff --git a/DemoApp/Sources/Components/Chat/DemoChatAdapter.swift b/DemoApp/Sources/Components/Chat/DemoChatAdapter.swift index 2b4af3970..628f7e38c 100644 --- a/DemoApp/Sources/Components/Chat/DemoChatAdapter.swift +++ b/DemoApp/Sources/Components/Chat/DemoChatAdapter.swift @@ -52,7 +52,7 @@ struct DemoChatAdapter { extension DemoChatAdapter { /// Returns the current value for the `StreamVideo` instance. struct InjectionKey: StreamChatSwiftUI.InjectionKey { - static var currentValue: DemoChatAdapter? + nonisolated(unsafe) static var currentValue: DemoChatAdapter? } } diff --git a/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift b/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift index fe1fcf76a..55aa0dad1 100644 --- a/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift +++ b/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift @@ -8,6 +8,7 @@ import StreamChatSwiftUI import class StreamVideoSwiftUI.CallViewModel import SwiftUI +@MainActor final class DemoChatViewFactory: ViewFactory { @Injected(\.chatClient) var chatClient: ChatClient diff --git a/DemoApp/Sources/Components/Chat/DemoChatViewModel+Injection.swift b/DemoApp/Sources/Components/Chat/DemoChatViewModel+Injection.swift index f825faa97..028666334 100644 --- a/DemoApp/Sources/Components/Chat/DemoChatViewModel+Injection.swift +++ b/DemoApp/Sources/Components/Chat/DemoChatViewModel+Injection.swift @@ -6,7 +6,7 @@ import Foundation import StreamVideo struct DemoChatViewModelInjectionKey: InjectionKey { - static var currentValue: DemoChatViewModel? + nonisolated(unsafe) static var currentValue: DemoChatViewModel? } extension InjectedValues { diff --git a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift index f934d164b..fb12c5293 100644 --- a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift +++ b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift @@ -214,9 +214,7 @@ final class ScannerViewController: UIViewController, UINavigationControllerDeleg reset() if (captureSession.isRunning == false) { - DispatchQueue.global(qos: .userInteractive).async { - self.captureSession?.startRunning() - } + self.captureSession?.startRunning() } } @@ -322,9 +320,7 @@ final class ScannerViewController: UIViewController, UINavigationControllerDeleg super.viewDidDisappear(animated) if (captureSession?.isRunning == true) { - DispatchQueue.global(qos: .userInteractive).async { - self.captureSession?.stopRunning() - } + captureSession?.stopRunning() } NotificationCenter.default.removeObserver(self) diff --git a/DemoApp/Sources/Components/Deeplinks/DeeplinkAdapter.swift b/DemoApp/Sources/Components/Deeplinks/DeeplinkAdapter.swift index 2a6d169e3..8eb1d560d 100644 --- a/DemoApp/Sources/Components/Deeplinks/DeeplinkAdapter.swift +++ b/DemoApp/Sources/Components/Deeplinks/DeeplinkAdapter.swift @@ -5,7 +5,7 @@ import Foundation import StreamVideo -struct DeeplinkInfo: Equatable { +struct DeeplinkInfo: Equatable, Sendable { var url: URL? var callId: String var callType: String diff --git a/DemoApp/Sources/Components/MemoryLogDestination/LogQueue.swift b/DemoApp/Sources/Components/MemoryLogDestination/LogQueue.swift index 5e3633f57..a6ad5b3d3 100644 --- a/DemoApp/Sources/Components/MemoryLogDestination/LogQueue.swift +++ b/DemoApp/Sources/Components/MemoryLogDestination/LogQueue.swift @@ -16,7 +16,7 @@ enum LogQueue { } } -final class Queue: ObservableObject { +final class Queue: ObservableObject, @unchecked Sendable { @Published private(set) var elements: [T] = [] var maxCount: Int { didSet { resizeIfNeeded() } } diff --git a/DemoApp/Sources/Components/Router.swift b/DemoApp/Sources/Components/Router.swift index 868382749..bcc24b6d9 100644 --- a/DemoApp/Sources/Components/Router.swift +++ b/DemoApp/Sources/Components/Router.swift @@ -13,7 +13,7 @@ import StreamVideoNoiseCancellation #endif @MainActor -final class Router: ObservableObject { +final class Router: ObservableObject, @unchecked Sendable { // MARK: - Properties @@ -234,9 +234,9 @@ final class Router: ObservableObject { appState.connectUser() } - private func refreshToken( + nonisolated private func refreshToken( for userId: String, - _ completionHandler: @escaping (Result) -> Void + _ completionHandler: @Sendable @escaping (Result) -> Void ) { Task { do { diff --git a/DemoApp/Sources/Components/SessionTimer/SessionTimer.swift b/DemoApp/Sources/Components/SessionTimer/SessionTimer.swift index df74068f1..1bb53851f 100644 --- a/DemoApp/Sources/Components/SessionTimer/SessionTimer.swift +++ b/DemoApp/Sources/Components/SessionTimer/SessionTimer.swift @@ -48,8 +48,8 @@ import SwiftUI } } - private var timer: Timer? - private var sessionEndCountdown: Timer? + nonisolated(unsafe) private var timer: Timer? + nonisolated(unsafe) private var sessionEndCountdown: Timer? private let alertInterval: TimeInterval @@ -132,8 +132,12 @@ import SwiftUI } } - deinit { + nonisolated private func cleanup() { timer?.invalidate() sessionEndCountdown?.invalidate() } + + deinit { + cleanup() + } } diff --git a/DemoApp/Sources/Components/Snapshot/LocalParticipantSnapshotViewModel.swift b/DemoApp/Sources/Components/Snapshot/LocalParticipantSnapshotViewModel.swift index 6ead61e79..b9661545e 100644 --- a/DemoApp/Sources/Components/Snapshot/LocalParticipantSnapshotViewModel.swift +++ b/DemoApp/Sources/Components/Snapshot/LocalParticipantSnapshotViewModel.swift @@ -9,7 +9,7 @@ import StreamVideo import UIKit final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDelegate, - AVCaptureVideoDataOutputSampleBufferDelegate { + AVCaptureVideoDataOutputSampleBufferDelegate, @unchecked Sendable { private actor State { private(set) var isCapturingVideoFrame = false diff --git a/DemoApp/Sources/Components/Snapshot/SnapshotTrigger.swift b/DemoApp/Sources/Components/Snapshot/SnapshotTrigger.swift index b4720d518..e8e7299ad 100644 --- a/DemoApp/Sources/Components/Snapshot/SnapshotTrigger.swift +++ b/DemoApp/Sources/Components/Snapshot/SnapshotTrigger.swift @@ -8,7 +8,7 @@ import StreamVideo import StreamVideoSwiftUI import SwiftUI -final class StreamSnapshotTrigger: SnapshotTriggering { +final class StreamSnapshotTrigger: SnapshotTriggering, @unchecked Sendable { lazy var binding: Binding = Binding( get: { [weak self] in self?.currentValueSubject.value ?? false diff --git a/DemoApp/Sources/Controls/DemoMoreControls/DemoReactionButton.swift b/DemoApp/Sources/Controls/DemoMoreControls/DemoReactionButton.swift index 5f22d6e4c..b9c48d519 100644 --- a/DemoApp/Sources/Controls/DemoMoreControls/DemoReactionButton.swift +++ b/DemoApp/Sources/Controls/DemoMoreControls/DemoReactionButton.swift @@ -19,7 +19,7 @@ struct DemoReactionSelectorView: View { var body: some View { HStack { - if orientationAdapter.orientation.isLandscape { + if orientationAdapter.orientation?.isLandscape == true { HStack {} .frame(maxWidth: .infinity) contentView diff --git a/DemoApp/Sources/Extensions/DemoApp+Sentry.swift b/DemoApp/Sources/Extensions/DemoApp+Sentry.swift index 5ca0123a6..5e3b514e0 100644 --- a/DemoApp/Sources/Extensions/DemoApp+Sentry.swift +++ b/DemoApp/Sources/Extensions/DemoApp+Sentry.swift @@ -46,7 +46,7 @@ func configureSentry() { } } -private final class SentryLogDestination: LogDestination { +private final class SentryLogDestination: LogDestination, @unchecked Sendable { func write(message: String) { // TODO: remove me once this function is gone from the protocol } diff --git a/DemoApp/Sources/ViewModifiers/DemoChatModifier.swift b/DemoApp/Sources/ViewModifiers/DemoChatModifier.swift index e94b08645..861db3e28 100644 --- a/DemoApp/Sources/ViewModifiers/DemoChatModifier.swift +++ b/DemoApp/Sources/ViewModifiers/DemoChatModifier.swift @@ -36,7 +36,7 @@ struct ChatModifier: ViewModifier { extension View { - @ViewBuilder + @MainActor @ViewBuilder func chat(viewModel: CallViewModel, chatViewModel: DemoChatViewModel?) -> some View { if let chatViewModel { modifier(ChatModifier(viewModel: viewModel, chatViewModel: chatViewModel)) diff --git a/DemoApp/Sources/ViewModifiers/MoreControls/DemoMoreControlsViewModifier.swift b/DemoApp/Sources/ViewModifiers/MoreControls/DemoMoreControlsViewModifier.swift index 8e6e25b5e..013c57493 100644 --- a/DemoApp/Sources/ViewModifiers/MoreControls/DemoMoreControlsViewModifier.swift +++ b/DemoApp/Sources/ViewModifiers/MoreControls/DemoMoreControlsViewModifier.swift @@ -217,7 +217,7 @@ struct DemoNoiseCancellationButtonView: View { extension View { - @ViewBuilder + @MainActor @ViewBuilder func presentsMoreControls(viewModel: CallViewModel) -> some View { modifier(DemoMoreControlsViewModifier(viewModel: viewModel)) } diff --git a/DemoApp/Sources/Views/Login/AddUserView.swift b/DemoApp/Sources/Views/Login/AddUserView.swift index 840da0bda..6a6939bad 100644 --- a/DemoApp/Sources/Views/Login/AddUserView.swift +++ b/DemoApp/Sources/Views/Login/AddUserView.swift @@ -224,7 +224,7 @@ struct DemoTextEditor: View { } @ViewBuilder - private func withPlaceholder( + @MainActor private func withPlaceholder( @ViewBuilder content: () -> some View ) -> some View { content() diff --git a/DemoApp/Sources/Views/Login/DebugMenu.swift b/DemoApp/Sources/Views/Login/DebugMenu.swift index 248ff1313..f081182f1 100644 --- a/DemoApp/Sources/Views/Login/DebugMenu.swift +++ b/DemoApp/Sources/Views/Login/DebugMenu.swift @@ -345,3 +345,5 @@ struct DebugMenu: View { } } } + +extension PerformanceMonitor.DisplayOptions: @unchecked Sendable {} diff --git a/DemoApp/Sources/Views/StatsView/DemoStatsView.swift b/DemoApp/Sources/Views/StatsView/DemoStatsView.swift index 2d29c5ef9..76f215b90 100644 --- a/DemoApp/Sources/Views/StatsView/DemoStatsView.swift +++ b/DemoApp/Sources/Views/StatsView/DemoStatsView.swift @@ -262,7 +262,7 @@ private struct DemoStatView: View { @Published var value: Value @Published var previousValue: Value - init( + @MainActor init( viewModel: CallViewModel, title: String = "", value: Value, @@ -396,7 +396,7 @@ private struct DemoLatencyChartView: View { @Published var values: [(offset: Int, element: Double)] = [] @Published var visibleRange: ClosedRange = 0...0 - init( + @MainActor init( viewModel: CallViewModel ) { self.viewModel = viewModel diff --git a/DemoAppUIKit/Sources/CallViewHelper.swift b/DemoAppUIKit/Sources/CallViewHelper.swift index a30a7fac9..c9edb94e2 100644 --- a/DemoAppUIKit/Sources/CallViewHelper.swift +++ b/DemoAppUIKit/Sources/CallViewHelper.swift @@ -4,6 +4,7 @@ import UIKit +@MainActor class CallViewHelper { static let shared = CallViewHelper() diff --git a/Sources/StreamVideo/Call.swift b/Sources/StreamVideo/Call.swift index 54c1e12be..1e8ae60cd 100644 --- a/Sources/StreamVideo/Call.swift +++ b/Sources/StreamVideo/Call.swift @@ -15,8 +15,6 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { private lazy var stateMachine: StreamCallStateMachine = .init(self) - @MainActor public internal(set) var state = CallState() - /// The call id. public let callId: String /// The call type. @@ -34,6 +32,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { /// Provides access to the speaker. public let speaker: SpeakerManager + @MainActor public internal(set) lazy var state = CallState() + internal let callController: CallController internal let coordinatorClient: DefaultAPI private var eventHandlers = [EventHandler]() diff --git a/Sources/StreamVideo/CallKit/CallKitAdapter.swift b/Sources/StreamVideo/CallKit/CallKitAdapter.swift index ac6ee23df..2835f5ab1 100644 --- a/Sources/StreamVideo/CallKit/CallKitAdapter.swift +++ b/Sources/StreamVideo/CallKit/CallKitAdapter.swift @@ -54,7 +54,7 @@ open class CallKitAdapter { extension CallKitAdapter: InjectionKey { /// Provides the current instance of `CallKitAdapter`. - public static var currentValue: CallKitAdapter = .init() + nonisolated(unsafe) public static var currentValue: CallKitAdapter = .init() } extension InjectedValues { diff --git a/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift b/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift index 76eadc5d8..01acc9d86 100644 --- a/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift +++ b/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift @@ -6,7 +6,7 @@ import Foundation import PushKit /// Handles push notifications for CallKit integration. -open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, ObservableObject { +@preconcurrency open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, ObservableObject { /// Represents the keys that the Payload dictionary public enum PayloadKey: String { @@ -157,7 +157,7 @@ open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, Obs extension CallKitPushNotificationAdapter: InjectionKey { /// Provides the current instance of `CallKitPushNotificationAdapter`. - public static var currentValue: CallKitPushNotificationAdapter = .init() + nonisolated(unsafe) public static var currentValue: CallKitPushNotificationAdapter = .init() } extension InjectedValues { diff --git a/Sources/StreamVideo/CallKit/CallKitService.swift b/Sources/StreamVideo/CallKit/CallKitService.swift index 0c6d7ca49..d423e5f0f 100644 --- a/Sources/StreamVideo/CallKit/CallKitService.swift +++ b/Sources/StreamVideo/CallKit/CallKitService.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // -import CallKit +@preconcurrency import CallKit import Combine import Foundation @@ -341,7 +341,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { """ ) do { - let rejectionReason = streamVideo? + let rejectionReason = await streamVideo? .rejectionReasonProvider .reason( for: stackEntry.call.cId, @@ -526,7 +526,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { extension CallKitService: InjectionKey { /// Provides the current instance of `CallKitService`. - public static var currentValue: CallKitService = .init() + nonisolated(unsafe) public static var currentValue: CallKitService = .init() } extension InjectedValues { diff --git a/Sources/StreamVideo/Controllers/CallController.swift b/Sources/StreamVideo/Controllers/CallController.swift index 57f509c08..3cb25931a 100644 --- a/Sources/StreamVideo/Controllers/CallController.swift +++ b/Sources/StreamVideo/Controllers/CallController.swift @@ -465,16 +465,18 @@ class CallController: @unchecked Sendable { private func handleParticipantsUpdated() { webRTCClient?.onParticipantsUpdated = { [weak self] participants in + guard let self else { return } DispatchQueue.main.async { - self?.call?.state.participantsMap = participants + self.call?.state.participantsMap = participants } } } private func handleParticipantCountUpdated() { webRTCClient?.onParticipantCountUpdated = { [weak self] participantCount in + guard let self else { return } DispatchQueue.main.async { - self?.call?.state.participantCount = participantCount + self.call?.state.participantCount = participantCount } } } diff --git a/Sources/StreamVideo/DependencyInjection/InjectedValues.swift b/Sources/StreamVideo/DependencyInjection/InjectedValues.swift index b3ce655a6..48876600e 100644 --- a/Sources/StreamVideo/DependencyInjection/InjectedValues.swift +++ b/Sources/StreamVideo/DependencyInjection/InjectedValues.swift @@ -15,7 +15,7 @@ public protocol InjectionKey { /// Provides access to injected dependencies. public struct InjectedValues { /// This is only used as an accessor to the computed properties within extensions of `InjectedValues`. - private static var current = InjectedValues() + nonisolated(unsafe) private static var current = InjectedValues() /// A static subscript for updating the `currentValue` of `InjectionKey` instances. public static subscript(key: K.Type) -> K.Value where K: InjectionKey { diff --git a/Sources/StreamVideo/Errors/Errors.swift b/Sources/StreamVideo/Errors/Errors.swift index dfe8043a9..361a183ab 100644 --- a/Sources/StreamVideo/Errors/Errors.swift +++ b/Sources/StreamVideo/Errors/Errors.swift @@ -4,7 +4,7 @@ import Foundation -extension APIError: Error {} +extension APIError: Error, @unchecked Sendable {} extension Stream_Video_Sfu_Models_Error: Error, CustomStringConvertible { var description: String { @@ -13,7 +13,7 @@ extension Stream_Video_Sfu_Models_Error: Error, CustomStringConvertible { } /// A Client error. -public class ClientError: Error, CustomStringConvertible { +public class ClientError: Error, CustomStringConvertible, @unchecked Sendable { public struct Location: Equatable { public let file: String public let line: Int @@ -73,19 +73,19 @@ public class ClientError: Error, CustomStringConvertible { extension ClientError { /// An unexpected error. - public class Unexpected: ClientError {} + public class Unexpected: ClientError, @unchecked Sendable {} /// An unknown error. - public class Unknown: ClientError {} + public class Unknown: ClientError, @unchecked Sendable {} /// Networking error. - public class NetworkError: ClientError {} + public class NetworkError: ClientError, @unchecked Sendable {} /// Permissions error. - public class MissingPermissions: ClientError {} + public class MissingPermissions: ClientError, @unchecked Sendable {} /// Invalid url error. - public class InvalidURL: ClientError {} + public class InvalidURL: ClientError, @unchecked Sendable {} } // This should probably live only in the test target since it's not "true" equatable diff --git a/Sources/StreamVideo/HTTPClient/HTTPConfig.swift b/Sources/StreamVideo/HTTPClient/HTTPConfig.swift index da2a9d852..b9b568aef 100644 --- a/Sources/StreamVideo/HTTPClient/HTTPConfig.swift +++ b/Sources/StreamVideo/HTTPClient/HTTPConfig.swift @@ -4,7 +4,7 @@ import Foundation -struct HTTPConfig { +struct HTTPConfig: Sendable { var retryStrategy: RetryStrategy let maxRetries: Int } diff --git a/Sources/StreamVideo/HTTPClient/InternetConnection.swift b/Sources/StreamVideo/HTTPClient/InternetConnection.swift index 2c1b1985f..9e024145d 100644 --- a/Sources/StreamVideo/HTTPClient/InternetConnection.swift +++ b/Sources/StreamVideo/HTTPClient/InternetConnection.swift @@ -151,7 +151,7 @@ extension InternetConnection.Status { extension InternetConnection { /// The default Internet connection monitor for iOS 12+. /// It uses Apple Network API. - class Monitor: InternetConnectionMonitor { + class Monitor: InternetConnectionMonitor, @unchecked Sendable { private var monitor: NWPathMonitor? private let queue = DispatchQueue(label: "io.getstream.internet-monitor") diff --git a/Sources/StreamVideo/Models/CallsQuery.swift b/Sources/StreamVideo/Models/CallsQuery.swift index 5af8ef717..a355c17ce 100644 --- a/Sources/StreamVideo/Models/CallsQuery.swift +++ b/Sources/StreamVideo/Models/CallsQuery.swift @@ -59,7 +59,7 @@ public enum CallSortDirection: Int { } /// Defines the field to sort calls by. -public struct CallSortField: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral { +public struct CallSortField: RawRepresentable, Codable, Hashable, ExpressibleByStringLiteral, Sendable { public let rawValue: String public init(rawValue: String) { diff --git a/Sources/StreamVideo/Models/CoordinatorModels.swift b/Sources/StreamVideo/Models/CoordinatorModels.swift index a84a31266..75e046b38 100644 --- a/Sources/StreamVideo/Models/CoordinatorModels.swift +++ b/Sources/StreamVideo/Models/CoordinatorModels.swift @@ -28,10 +28,11 @@ extension PinResponse: @unchecked Sendable {} extension UnpinResponse: @unchecked Sendable {} extension GoLiveResponse: @unchecked Sendable {} extension SendReactionResponse: @unchecked Sendable {} +extension CallStateResponseFields: @unchecked Sendable {} public struct FetchingLocationError: Error {} -public enum RecordingState { +public enum RecordingState: Sendable { case noRecording case requested case recording diff --git a/Sources/StreamVideo/Models/Token.swift b/Sources/StreamVideo/Models/Token.swift index 8ebfe63bd..397aa6f5a 100644 --- a/Sources/StreamVideo/Models/Token.swift +++ b/Sources/StreamVideo/Models/Token.swift @@ -30,7 +30,7 @@ public struct UserToken: Codable, Equatable, ExpressibleByStringLiteral, Sendabl } extension ClientError { - public class InvalidToken: ClientError {} + public class InvalidToken: ClientError, @unchecked Sendable {} } public extension UserToken { diff --git a/Sources/StreamVideo/OpenApi/generated/CodableHelper.swift b/Sources/StreamVideo/OpenApi/generated/CodableHelper.swift index 09c82e53e..701d175ec 100644 --- a/Sources/StreamVideo/OpenApi/generated/CodableHelper.swift +++ b/Sources/StreamVideo/OpenApi/generated/CodableHelper.swift @@ -8,18 +8,18 @@ import Foundation open class CodableHelper { - private static var customDateFormatter: DateFormatter? - private static var defaultDateFormatter: DateFormatter = OpenISO8601DateFormatter() + nonisolated(unsafe) private static var customDateFormatter: DateFormatter? + nonisolated(unsafe) private static var defaultDateFormatter: DateFormatter = OpenISO8601DateFormatter() - private static var customJSONDecoder: JSONDecoder? - private static var defaultJSONDecoder: JSONDecoder = { + nonisolated(unsafe) private static var customJSONDecoder: JSONDecoder? + nonisolated(unsafe) private static var defaultJSONDecoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(CodableHelper.dateFormatter) return decoder }() - private static var customJSONEncoder: JSONEncoder? - private static var defaultJSONEncoder: JSONEncoder = { + nonisolated(unsafe) private static var customJSONEncoder: JSONEncoder? + nonisolated(unsafe) private static var defaultJSONEncoder: JSONEncoder = { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .formatted(CodableHelper.dateFormatter) encoder.outputFormatting = .prettyPrinted diff --git a/Sources/StreamVideo/StreamVideo.swift b/Sources/StreamVideo/StreamVideo.swift index b633d3ea4..27e1788b9 100644 --- a/Sources/StreamVideo/StreamVideo.swift +++ b/Sources/StreamVideo/StreamVideo.swift @@ -7,8 +7,8 @@ import Foundation import StreamWebRTC import SwiftProtobuf -public typealias UserTokenProvider = (@escaping (Result) -> Void) -> Void -public typealias UserTokenUpdater = (UserToken) -> Void +public typealias UserTokenProvider = @Sendable(@Sendable @escaping (Result) -> Void) -> Void +public typealias UserTokenUpdater = @Sendable(UserToken) -> Void /// Main class for interacting with the `StreamVideo` SDK. /// Needs to be initalized with a valid api key, user and token (and token provider). @@ -16,7 +16,7 @@ public class StreamVideo: ObservableObject, @unchecked Sendable { @Injected(\.callCache) private var callCache - public class State: ObservableObject { + public final class State: ObservableObject, @unchecked Sendable { @Published public internal(set) var connection: ConnectionStatus = .initialized @Published public internal(set) var user: User @Published public internal(set) var activeCall: Call? { @@ -536,7 +536,7 @@ public class StreamVideo: ObservableObject, @unchecked Sendable { customData: updatedUser.customData ) } - let tokenProvider = { [environment = self.environment] result in + let tokenProvider = { @Sendable [environment = self.environment] result in Self.loadGuestToken( userId: user.id, apiKey: apiKey, @@ -611,7 +611,7 @@ public class StreamVideo: ObservableObject, @unchecked Sendable { userId: String, apiKey: String, environment: Environment, - result: @escaping (Result) -> Void + result: @Sendable @escaping (Result) -> Void ) { Task { do { @@ -701,5 +701,5 @@ extension StreamVideo: WSEventsSubscriber { /// Returns the current value for the `StreamVideo` instance. struct StreamVideoProviderKey: InjectionKey { - static var currentValue: StreamVideo? + nonisolated(unsafe) static var currentValue: StreamVideo? } diff --git a/Sources/StreamVideo/StreamVideoEnvironment.swift b/Sources/StreamVideo/StreamVideoEnvironment.swift index 9ae922211..ef9fcc3c4 100644 --- a/Sources/StreamVideo/StreamVideoEnvironment.swift +++ b/Sources/StreamVideo/StreamVideoEnvironment.swift @@ -5,7 +5,7 @@ import Foundation extension StreamVideo { - struct Environment { + struct Environment: @unchecked Sendable { var webSocketClientBuilder: ( _ eventNotificationCenter: EventNotificationCenter, _ url: URL @@ -69,7 +69,7 @@ extension StreamVideo { ) } - private static var backgroundTaskSchedulerBuilder: () -> BackgroundTaskScheduler? = { + nonisolated(unsafe) private static var backgroundTaskSchedulerBuilder: () -> BackgroundTaskScheduler? = { if Bundle.main.isAppExtension { // No background task scheduler exists for app extensions. return nil diff --git a/Sources/StreamVideo/Utils/AudioRecorder/StreamActiveCallProvider.swift b/Sources/StreamVideo/Utils/AudioRecorder/StreamActiveCallProvider.swift index 4bc7dd6ab..c27557403 100644 --- a/Sources/StreamVideo/Utils/AudioRecorder/StreamActiveCallProvider.swift +++ b/Sources/StreamVideo/Utils/AudioRecorder/StreamActiveCallProvider.swift @@ -21,7 +21,7 @@ extension StreamVideo: StreamActiveCallProviding { /// Provides the default value of the `StreamActiveCallProviding` class. struct StreamActiveCallProviderKey: InjectionKey { - static var currentValue: StreamActiveCallProviding? + nonisolated(unsafe) static var currentValue: StreamActiveCallProviding? } extension InjectedValues { diff --git a/Sources/StreamVideo/Utils/AudioRecorder/StreamCallAudioRecorder.swift b/Sources/StreamVideo/Utils/AudioRecorder/StreamCallAudioRecorder.swift index dc74b3f19..66c68eaf3 100644 --- a/Sources/StreamVideo/Utils/AudioRecorder/StreamCallAudioRecorder.swift +++ b/Sources/StreamVideo/Utils/AudioRecorder/StreamCallAudioRecorder.swift @@ -226,7 +226,7 @@ open class StreamCallAudioRecorder: @unchecked Sendable { /// Provides the default value of the `StreamCallAudioRecorder` class. struct StreamCallAudioRecorderKey: InjectionKey { - static var currentValue: StreamCallAudioRecorder = StreamCallAudioRecorder( + nonisolated(unsafe) static var currentValue: StreamCallAudioRecorder = StreamCallAudioRecorder( filename: "recording.wav" ) } diff --git a/Sources/StreamVideo/Utils/CallCache/CallCache.swift b/Sources/StreamVideo/Utils/CallCache/CallCache.swift index 915293def..c70b241ce 100644 --- a/Sources/StreamVideo/Utils/CallCache/CallCache.swift +++ b/Sources/StreamVideo/Utils/CallCache/CallCache.swift @@ -54,7 +54,7 @@ final class CallCache { } extension CallCache: InjectionKey { - static var currentValue: CallCache = .init() + nonisolated(unsafe) static var currentValue: CallCache = .init() } extension InjectedValues { diff --git a/Sources/StreamVideo/Utils/Logger/Destination/BaseLogDestination.swift b/Sources/StreamVideo/Utils/Logger/Destination/BaseLogDestination.swift index f8ae3d42f..742c33a3f 100644 --- a/Sources/StreamVideo/Utils/Logger/Destination/BaseLogDestination.swift +++ b/Sources/StreamVideo/Utils/Logger/Destination/BaseLogDestination.swift @@ -6,7 +6,7 @@ import Foundation /// Base class for log destinations. Already implements basic functionality to allow easy destination implementation. /// Extending this class, instead of implementing `LogDestination` is easier (and recommended) for creating new destinations. -open class BaseLogDestination: LogDestination { +open class BaseLogDestination: LogDestination, @unchecked Sendable { open var identifier: String open var level: LogLevel open var subsystems: LogSubsystem diff --git a/Sources/StreamVideo/Utils/Logger/Destination/ConsoleLogDestination.swift b/Sources/StreamVideo/Utils/Logger/Destination/ConsoleLogDestination.swift index fcaeaace2..727530c1e 100644 --- a/Sources/StreamVideo/Utils/Logger/Destination/ConsoleLogDestination.swift +++ b/Sources/StreamVideo/Utils/Logger/Destination/ConsoleLogDestination.swift @@ -5,7 +5,7 @@ import Foundation /// Basic destination for outputting messages to console. -public class ConsoleLogDestination: BaseLogDestination { +public class ConsoleLogDestination: BaseLogDestination, @unchecked Sendable { override open func write(message: String) { print(message) } diff --git a/Sources/StreamVideo/Utils/Logger/Destination/LogDestination.swift b/Sources/StreamVideo/Utils/Logger/Destination/LogDestination.swift index 81de97fdc..453993e3d 100644 --- a/Sources/StreamVideo/Utils/Logger/Destination/LogDestination.swift +++ b/Sources/StreamVideo/Utils/Logger/Destination/LogDestination.swift @@ -6,7 +6,7 @@ import Foundation /// Log level for any messages to be logged. /// Please check [this Apple Logging Article](https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code) to understand different logging levels. -public enum LogLevel: Int { +public enum LogLevel: Int, Sendable { /// Use this log level if you want to see everything that is logged. case debug = 0 /// Use this log level if you want to see what is happening during the app execution. @@ -18,7 +18,7 @@ public enum LogLevel: Int { } /// Encapsulates the components of a log message. -public struct LogDetails { +public struct LogDetails: Sendable { public let loggerIdentifier: String public let subsystem: LogSubsystem @@ -33,7 +33,7 @@ public struct LogDetails { public let error: Error? } -public protocol LogDestination { +public protocol LogDestination: Sendable { var identifier: String { get set } var level: LogLevel { get set } var subsystems: LogSubsystem { get set } diff --git a/Sources/StreamVideo/Utils/Logger/Logger.swift b/Sources/StreamVideo/Utils/Logger/Logger.swift index d3c1706e6..1035e4fb2 100644 --- a/Sources/StreamVideo/Utils/Logger/Logger.swift +++ b/Sources/StreamVideo/Utils/Logger/Logger.swift @@ -9,7 +9,7 @@ public var log: Logger { } /// Entity for identifying which subsystem the log message comes from. -public struct LogSubsystem: OptionSet, CustomStringConvertible { +public struct LogSubsystem: OptionSet, CustomStringConvertible, @unchecked Sendable { public let rawValue: Int public init(rawValue: Int) { @@ -78,21 +78,21 @@ public struct LogSubsystem: OptionSet, CustomStringConvertible { public enum LogConfig { /// Identifier for the logger. Defaults to empty. - public static var identifier = "" { + nonisolated(unsafe) public static var identifier = "" { didSet { invalidateLogger() } } /// Output level for the logger. - public static var level: LogLevel = .error { + nonisolated(unsafe) public static var level: LogLevel = .error { didSet { invalidateLogger() } } /// Date formatter for the logger. Defaults to ISO8601 - public static var dateFormatter: DateFormatter = { + nonisolated(unsafe) public static var dateFormatter: DateFormatter = { let df = DateFormatter() df.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" return df @@ -104,63 +104,63 @@ public enum LogConfig { /// Log formatters to be applied in order before logs are outputted. Defaults to empty (no formatters). /// Please see `LogFormatter` for more info. - public static var formatters = [LogFormatter]() { + nonisolated(unsafe) public static var formatters = [LogFormatter]() { didSet { invalidateLogger() } } /// Toggle for showing date in logs - public static var showDate = true { + nonisolated(unsafe) public static var showDate = true { didSet { invalidateLogger() } } /// Toggle for showing log level in logs - public static var showLevel = true { + nonisolated(unsafe) public static var showLevel = true { didSet { invalidateLogger() } } /// Toggle for showing identifier in logs - public static var showIdentifier = false { + nonisolated(unsafe) public static var showIdentifier = false { didSet { invalidateLogger() } } /// Toggle for showing thread name in logs - public static var showThreadName = true { + nonisolated(unsafe) public static var showThreadName = true { didSet { invalidateLogger() } } /// Toggle for showing file name in logs - public static var showFileName = true { + nonisolated(unsafe) public static var showFileName = true { didSet { invalidateLogger() } } /// Toggle for showing line number in logs - public static var showLineNumber = true { + nonisolated(unsafe) public static var showLineNumber = true { didSet { invalidateLogger() } } /// Toggle for showing function name in logs - public static var showFunctionName = true { + nonisolated(unsafe) public static var showFunctionName = true { didSet { invalidateLogger() } } /// Subsystems for the logger - public static var subsystems: LogSubsystem = .all { + nonisolated(unsafe) public static var subsystems: LogSubsystem = .all { didSet { invalidateLogger() } @@ -170,13 +170,13 @@ public enum LogConfig { /// /// Logger will initialize the destinations with its own parameters. If you want full control on the parameters, use `destinations` directly, /// where you can pass parameters to destination initializers yourself. - public static var destinationTypes: [LogDestination.Type] = [ConsoleLogDestination.self] { + nonisolated(unsafe) public static var destinationTypes: [LogDestination.Type] = [ConsoleLogDestination.self] { didSet { invalidateLogger() } } - private static var _destinations: [LogDestination]? + nonisolated(unsafe) private static var _destinations: [LogDestination]? /// Destinations for the default logger. Please see `LogDestination`. /// Defaults to only `ConsoleLogDestination`, which only prints the messages. @@ -213,7 +213,7 @@ public enum LogConfig { } /// Underlying logger instance to control singleton. - private static var _logger: Logger? + nonisolated(unsafe) private static var _logger: Logger? /// Logger instance to be used by StreamChat. /// @@ -277,7 +277,7 @@ public class Logger { log( level, functionName: functionName, - fileName: fileName, + fileName: (fileName), lineNumber: lineNumber, message: message(), subsystems: subsystems, diff --git a/Sources/StreamVideo/Utils/ParticipantAutoLeavePolicy/LastParticipantAutoLeavePolicy.swift b/Sources/StreamVideo/Utils/ParticipantAutoLeavePolicy/LastParticipantAutoLeavePolicy.swift index bb9f1090f..539e07ef8 100644 --- a/Sources/StreamVideo/Utils/ParticipantAutoLeavePolicy/LastParticipantAutoLeavePolicy.swift +++ b/Sources/StreamVideo/Utils/ParticipantAutoLeavePolicy/LastParticipantAutoLeavePolicy.swift @@ -7,7 +7,7 @@ import Foundation /// A policy that triggers an action when during a ringing flow call (incoming or outgoing) there is only one /// participant left in a call, after having previously had multiple participants. -public final class LastParticipantAutoLeavePolicy: ParticipantAutoLeavePolicy { +public final class LastParticipantAutoLeavePolicy: ParticipantAutoLeavePolicy, @unchecked Sendable { /// Injected dependency for accessing the stream video service. @Injected(\.streamVideo) private var streamVideo diff --git a/Sources/StreamVideo/Utils/RawJSON.swift b/Sources/StreamVideo/Utils/RawJSON.swift index b68504ef1..d7dc45b82 100644 --- a/Sources/StreamVideo/Utils/RawJSON.swift +++ b/Sources/StreamVideo/Utils/RawJSON.swift @@ -15,7 +15,7 @@ public indirect enum RawJSON: Codable, Hashable, Sendable { case array([RawJSON]) case `nil` - static let double = number + nonisolated(unsafe) static let double = number public init(from decoder: Decoder) throws { let singleValueContainer = try decoder.singleValueContainer() diff --git a/Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift b/Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift index 421aba751..422837e9b 100644 --- a/Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift +++ b/Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift @@ -19,7 +19,7 @@ public protocol RejectionReasonProviding { /// /// - Note: ``ringTimeout`` being true, has an effect **only** when it's set from the side of /// the caller when the callee doesn't reply the ringing call in the amount of time set on the dashboard. - func reason(for callCid: String, ringTimeout: Bool) -> String? + @MainActor func reason(for callCid: String, ringTimeout: Bool) -> String? } /// A provider that determines the rejection reason for a call based on its state. diff --git a/Sources/StreamVideo/Utils/Sorting.swift b/Sources/StreamVideo/Utils/Sorting.swift index 5a2296196..a6ecf502d 100644 --- a/Sources/StreamVideo/Utils/Sorting.swift +++ b/Sources/StreamVideo/Utils/Sorting.swift @@ -5,7 +5,7 @@ import Foundation /// A type representing a comparator function that compares two `Value` objects and returns a `ComparisonResult`. -public typealias StreamSortComparator = (Value, Value) -> ComparisonResult +public typealias StreamSortComparator = @Sendable(Value, Value) -> ComparisonResult /// The default set of comparators used for sorting `CallParticipant` objects. /// - `pinned`: Prioritizes participants who are pinned. @@ -155,7 +155,7 @@ public func combineComparators(_ comparators: [StreamSortComparator]) -> S /// Returns a new comparator that only applies the given comparator if the predicate returns true. /// If the predicate returns false, it deems the two elements "equal" (i.e., orderedSame). -public func conditional(_ predicate: @escaping (T, T) -> Bool) +public func conditional(_ predicate: @Sendable @escaping (T, T) -> Bool) -> (@escaping StreamSortComparator) -> StreamSortComparator { { comparator in { a, b in @@ -168,21 +168,21 @@ public func conditional(_ predicate: @escaping (T, T) -> Bool) } /// A specific conditional comparator for CallParticipant that checks if either participant's track is not visible. -public let ifInvisible = conditional { (lhs: CallParticipant, rhs: CallParticipant) -> Bool in +nonisolated(unsafe) public let ifInvisible = conditional { (lhs: CallParticipant, rhs: CallParticipant) -> Bool in !lhs.showTrack || !rhs.showTrack } // MARK: Instance /// Comparator which sorts participants by the fact that they are the dominant speaker or not. -public var dominantSpeaker: StreamSortComparator = { comparison($0, $1, keyPath: \.isDominantSpeaker) } +public let dominantSpeaker: StreamSortComparator = { comparison($0, $1, keyPath: \.isDominantSpeaker) } /// Comparator which sorts participants by the fact that they are speaking or not. -public var isSpeaking: StreamSortComparator = { comparison($0, $1, keyPath: \.isSpeaking) } +public let isSpeaking: StreamSortComparator = { comparison($0, $1, keyPath: \.isSpeaking) } /// Comparator which prioritizes participants who are pinned. /// - Note: Remote pins have higher priority than local. -public var pinned: StreamSortComparator = { a, b in +public let pinned: StreamSortComparator = { a, b in switch (a.pin, b.pin) { case (nil, _?): return .orderedDescending case (_?, nil): return .orderedAscending @@ -195,16 +195,16 @@ public var pinned: StreamSortComparator = { a, b in } /// Comparator which sorts participants by screen sharing status. -public var screensharing: StreamSortComparator = { comparison($0, $1, keyPath: \.isScreensharing) } +public let screensharing: StreamSortComparator = { comparison($0, $1, keyPath: \.isScreensharing) } /// Comparator which sorts participants by video status. -public var publishingVideo: StreamSortComparator = { comparison($0, $1, keyPath: \.hasVideo) } +public let publishingVideo: StreamSortComparator = { comparison($0, $1, keyPath: \.hasVideo) } /// Comparator which sorts participants by audio status. -public var publishingAudio: StreamSortComparator = { comparison($0, $1, keyPath: \.hasAudio) } +public let publishingAudio: StreamSortComparator = { comparison($0, $1, keyPath: \.hasAudio) } /// Comparator which sorts participants by name. -public var name: StreamSortComparator = { comparison($0, $1, keyPath: \.name) } +public let name: StreamSortComparator = { comparison($0, $1, keyPath: \.name) } /// A comparator creator which will set up a comparator which prioritizes participants who have a specific role. public func roles(_ priorityRoles: [String] = ["admin", "host", "speaker"]) -> StreamSortComparator { @@ -223,10 +223,10 @@ public func roles(_ priorityRoles: [String] = ["admin", "host", "speaker"]) -> S } /// Comparator for sorting `CallParticipant` objects based on their `id` property -public var id: StreamSortComparator = { comparison($0, $1, keyPath: \.id) } +public let id: StreamSortComparator = { comparison($0, $1, keyPath: \.id) } /// Comparator for sorting `CallParticipant` objects based on their `userId` property -public var userId: StreamSortComparator = { comparison($0, $1, keyPath: \.userId) } +public let userId: StreamSortComparator = { comparison($0, $1, keyPath: \.userId) } /// Comparator for sorting `CallParticipant` objects based on the date and time (`joinedAt`) they joined the call -public var joinedAt: StreamSortComparator = { comparison($0, $1, keyPath: \.joinedAt) } +public let joinedAt: StreamSortComparator = { comparison($0, $1, keyPath: \.joinedAt) } diff --git a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+AcceptingStage.swift b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+AcceptingStage.swift index baca5b43d..4cd8c1f62 100644 --- a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+AcceptingStage.swift +++ b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+AcceptingStage.swift @@ -21,7 +21,7 @@ extension StreamCallStateMachine.Stage { extension StreamCallStateMachine.Stage { /// A class representing the accepting stage in the `StreamCallStateMachine`. - final class AcceptingStage: StreamCallStateMachine.Stage { + final class AcceptingStage: StreamCallStateMachine.Stage, @unchecked Sendable { let actionBlock: () async throws -> AcceptCallResponse /// Initializes a new accepting stage with the provided call and action block. diff --git a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+Stage.swift b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+Stage.swift index e90101381..26c5f1d09 100644 --- a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+Stage.swift +++ b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+Stage.swift @@ -6,7 +6,7 @@ import Foundation extension StreamCallStateMachine { /// A class representing a stage in the `StreamCallStateMachine`. - class Stage: StreamStateMachineStage { + class Stage: StreamStateMachineStage, @unchecked Sendable { /// Enumeration of possible stage identifiers. enum ID: Hashable, CaseIterable { diff --git a/Sources/StreamVideo/Utils/StreamRuntimeCheck.swift b/Sources/StreamVideo/Utils/StreamRuntimeCheck.swift index 18314dbbe..90c885958 100644 --- a/Sources/StreamVideo/Utils/StreamRuntimeCheck.swift +++ b/Sources/StreamVideo/Utils/StreamRuntimeCheck.swift @@ -8,5 +8,5 @@ public enum StreamRuntimeCheck { /// Enables assertions thrown by the StreamVideo SDK. /// /// When set to false, a message will be logged on console, but the assertion will not be thrown. - static var assertionsEnabled = false + nonisolated(unsafe) static var assertionsEnabled = false } diff --git a/Sources/StreamVideo/Utils/SystemEnvironment+XStreamClient.swift b/Sources/StreamVideo/Utils/SystemEnvironment+XStreamClient.swift index c43c94d74..0562bef13 100644 --- a/Sources/StreamVideo/Utils/SystemEnvironment+XStreamClient.swift +++ b/Sources/StreamVideo/Utils/SystemEnvironment+XStreamClient.swift @@ -12,7 +12,7 @@ import IOKit extension SystemEnvironment { static let xStreamClientHeader: String = { - "stream-video-swift-client-v\(version)|app=\(appName)|app_version=\(appVersion)|os=\(os) \(osVersion)|device_model=\(model)|device_screen_ratio=\(scale)" + "stream-video-swift-client-v\(version)|app=\(appName)|app_version=\(appVersion)|os=\(os) \(osVersion)|device_model=\(model)" }() static let clientDetails: Stream_Video_Sfu_Models_ClientDetails = { @@ -68,11 +68,7 @@ extension SystemEnvironment { #endif private static var osVersion: String { - #if os(iOS) - return UIDevice.current.systemVersion - #elseif os(macOS) - return ProcessInfo.processInfo.operatingSystemVersionString - #endif + ProcessInfo.processInfo.operatingSystemVersionString } private static var os: String { @@ -82,14 +78,6 @@ extension SystemEnvironment { return "MacOS" #endif } - - private static var scale: String { - #if os(iOS) - return String(format: "%0.2f", UIScreen.main.scale) - #elseif os(macOS) - return "1.00" - #endif - } } extension Array { diff --git a/Sources/StreamVideo/Utils/ThermalStateObserver.swift b/Sources/StreamVideo/Utils/ThermalStateObserver.swift index 7da71ca58..f7a9ba8b8 100644 --- a/Sources/StreamVideo/Utils/ThermalStateObserver.swift +++ b/Sources/StreamVideo/Utils/ThermalStateObserver.swift @@ -34,7 +34,7 @@ public protocol ThermalStateObserving: ObservableObject { /// `ThermalStateObserver` monitors the device's thermal state and provides both immediate access to the current state /// and a publisher for tracking state changes over time. It also offers a derived scaling factor to help adapt app behavior /// or features based on the current thermal conditions. -final class ThermalStateObserver: ObservableObject, ThermalStateObserving { +final class ThermalStateObserver: ObservableObject, ThermalStateObserving, @unchecked Sendable { static let shared = ThermalStateObserver() /// Published property to observe the thermal state @@ -110,7 +110,7 @@ final class ThermalStateObserver: ObservableObject, ThermalStateObserving { /// Provides the default value of the `Appearance` class. enum ThermalStateObserverKey: InjectionKey { - static var currentValue: any ThermalStateObserving = ThermalStateObserver.shared + nonisolated(unsafe) static var currentValue: any ThermalStateObserving = ThermalStateObserver.shared } extension InjectedValues { diff --git a/Sources/StreamVideo/Utils/Timers.swift b/Sources/StreamVideo/Utils/Timers.swift index cc4fe1488..5cec3846d 100644 --- a/Sources/StreamVideo/Utils/Timers.swift +++ b/Sources/StreamVideo/Utils/Timers.swift @@ -77,7 +77,7 @@ struct DefaultTimer: Timer { } } -private class RepeatingTimer: RepeatingTimerControl { +private class RepeatingTimer: RepeatingTimerControl, @unchecked Sendable { private enum State { case suspended case resumed diff --git a/Sources/StreamVideo/Utils/UUIDProviding/StreamUUIDFactory.swift b/Sources/StreamVideo/Utils/UUIDProviding/StreamUUIDFactory.swift index c75348af4..6bb0d5cfa 100644 --- a/Sources/StreamVideo/Utils/UUIDProviding/StreamUUIDFactory.swift +++ b/Sources/StreamVideo/Utils/UUIDProviding/StreamUUIDFactory.swift @@ -13,7 +13,7 @@ public protocol UUIDProviding { /// A key used for dependency injection of UUID providers. public enum UUIDProviderKey: InjectionKey { /// The current value of UUID provider, defaulted to `StreamUUIDFactory`. - public static var currentValue: UUIDProviding = StreamUUIDFactory() + nonisolated(unsafe) public static var currentValue: UUIDProviding = StreamUUIDFactory() } extension InjectedValues { diff --git a/Sources/StreamVideo/Utils/Utils.swift b/Sources/StreamVideo/Utils/Utils.swift index f55f2b400..540f89731 100644 --- a/Sources/StreamVideo/Utils/Utils.swift +++ b/Sources/StreamVideo/Utils/Utils.swift @@ -35,7 +35,7 @@ struct EventHandler { var cancel: () -> Void } -func executeOnMain(_ task: @escaping @MainActor() -> Void) { +func executeOnMain(_ task: @Sendable @escaping @MainActor() -> Void) { Task { await task() } diff --git a/Sources/StreamVideo/WebRTC/AudioFilter/StreamAudioProcessingModule.swift b/Sources/StreamVideo/WebRTC/AudioFilter/StreamAudioProcessingModule.swift index 19fae7b4e..417ba053a 100644 --- a/Sources/StreamVideo/WebRTC/AudioFilter/StreamAudioProcessingModule.swift +++ b/Sources/StreamVideo/WebRTC/AudioFilter/StreamAudioProcessingModule.swift @@ -65,7 +65,7 @@ open class StreamAudioFilterProcessingModule: RTCDefaultAudioProcessingModule, A } enum AudioProcessingModuleKey: InjectionKey { - public static var currentValue: AudioProcessingModule = StreamAudioFilterProcessingModule() + nonisolated(unsafe) public static var currentValue: AudioProcessingModule = StreamAudioFilterProcessingModule() } extension InjectedValues { diff --git a/Sources/StreamVideo/WebRTC/AudioSession.swift b/Sources/StreamVideo/WebRTC/AudioSession.swift index f91456491..1af74559b 100644 --- a/Sources/StreamVideo/WebRTC/AudioSession.swift +++ b/Sources/StreamVideo/WebRTC/AudioSession.swift @@ -3,7 +3,7 @@ // import Foundation -import StreamWebRTC +@preconcurrency import StreamWebRTC actor AudioSession { @@ -41,13 +41,17 @@ actor AudioSession { } deinit { + cleanup() + } + + nonisolated private func cleanup() { rtcAudioSession.lockForConfiguration() rtcAudioSession.isAudioEnabled = false rtcAudioSession.unlockForConfiguration() } } -extension RTCAudioSessionConfiguration { +extension RTCAudioSessionConfiguration: @unchecked Sendable { static let `default`: RTCAudioSessionConfiguration = { let configuration = RTCAudioSessionConfiguration.webRTC() diff --git a/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift b/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift index 1c7474026..222f827d1 100644 --- a/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift +++ b/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift @@ -3,9 +3,9 @@ // import Foundation -import StreamWebRTC +@preconcurrency import StreamWebRTC -extension RTCMediaConstraints { +extension RTCMediaConstraints: @unchecked Sendable { static let defaultConstraints = RTCMediaConstraints( mandatoryConstraints: nil, diff --git a/Sources/StreamVideo/WebRTC/PeerConnection.swift b/Sources/StreamVideo/WebRTC/PeerConnection.swift index f2dcd6036..781bc6dfe 100644 --- a/Sources/StreamVideo/WebRTC/PeerConnection.swift +++ b/Sources/StreamVideo/WebRTC/PeerConnection.swift @@ -3,7 +3,7 @@ // import Foundation -import StreamWebRTC +@preconcurrency import StreamWebRTC import SwiftProtobuf enum PeerConnectionType: String { @@ -735,3 +735,5 @@ extension RTCSdpType: CustomStringConvertible { } } } + +extension RTCVideoTrack: @unchecked Sendable {} diff --git a/Sources/StreamVideo/WebRTC/Retries.swift b/Sources/StreamVideo/WebRTC/Retries.swift index 86a77e2a0..eb0753686 100644 --- a/Sources/StreamVideo/WebRTC/Retries.swift +++ b/Sources/StreamVideo/WebRTC/Retries.swift @@ -45,7 +45,7 @@ func executeTask( } } -struct RetryPolicy { +struct RetryPolicy: @unchecked Sendable { let maxRetries: Int let delay: (Int) -> TimeInterval var runPrecondition: () async -> Bool = { true } diff --git a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferConnection.swift b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferConnection.swift index 8641c5ac0..d797e20b9 100644 --- a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferConnection.swift +++ b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferConnection.swift @@ -5,7 +5,7 @@ import Foundation import StreamWebRTC -class BroadcastBufferConnection: NSObject { +class BroadcastBufferConnection: NSObject, @unchecked Sendable { var inputStream: InputStream? var outputStream: OutputStream? diff --git a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferReader.swift b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferReader.swift index 2197e7209..a9d83367f 100644 --- a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferReader.swift +++ b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferReader.swift @@ -227,3 +227,5 @@ extension BroadcastBufferReader: StreamDelegate { } } } + +extension CIContext: @unchecked Sendable {} diff --git a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferUploader.swift b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferUploader.swift index 0806e8706..b305b5e5c 100644 --- a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferUploader.swift +++ b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferUploader.swift @@ -8,7 +8,7 @@ import StreamWebRTC actor BroadcastBufferUploader { - private static var imageContext = CIContext(options: nil) + private static let imageContext = CIContext(options: nil) private var isReady = false private var connection: BroadcastBufferUploadConnection diff --git a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastSampleHandler.swift b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastSampleHandler.swift index bb99808bd..6e5bfae5b 100644 --- a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastSampleHandler.swift +++ b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastSampleHandler.swift @@ -5,7 +5,7 @@ import ReplayKit /// Handler that can be used in broadcast upload extension to support screensharing from the background. -open class BroadcastSampleHandler: RPBroadcastSampleHandler { +open class BroadcastSampleHandler: RPBroadcastSampleHandler, @unchecked Sendable { /// Represents the client connection for uploading broadcast buffers. private var clientConnection: BroadcastBufferUploadConnection? diff --git a/Sources/StreamVideo/WebRTC/SfuMiddleware.swift b/Sources/StreamVideo/WebRTC/SfuMiddleware.swift index dad8f5695..10d67c843 100644 --- a/Sources/StreamVideo/WebRTC/SfuMiddleware.swift +++ b/Sources/StreamVideo/WebRTC/SfuMiddleware.swift @@ -5,7 +5,7 @@ import Foundation import StreamWebRTC -class SfuMiddleware: EventMiddleware { +class SfuMiddleware: EventMiddleware, @unchecked Sendable { private let recordingUserId = "recording-egress" private let participantsThreshold: Int private let sessionID: String diff --git a/Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift b/Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift index ea4832e9e..27e8f5b3c 100644 --- a/Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift +++ b/Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift @@ -3,9 +3,9 @@ // import Foundation -import StreamWebRTC +@preconcurrency import StreamWebRTC -final class SimulatorScreenCapturer: RTCVideoCapturer { +final class SimulatorScreenCapturer: RTCVideoCapturer, @unchecked Sendable { private var displayLink: CADisplayLink? private var videoURL: URL private let queue = DispatchQueue(label: "org.webrtc.RTCFileVideoCapturer") @@ -96,7 +96,7 @@ final class SimulatorScreenCapturer: RTCVideoCapturer { /// Provides the default value of the `SimulatorStreamFile` class. enum SimulatorStreamFileKey: InjectionKey { - static var currentValue: URL? + nonisolated(unsafe) static var currentValue: URL? } extension InjectedValues { diff --git a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift index 1eeb14fcd..127faea83 100644 --- a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift +++ b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift @@ -29,12 +29,14 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { context = CIContext(options: [CIContextOption.useSoftwareRenderer: false]) colorSpace = CGColorSpaceCreateDeviceRGB() super.init() - NotificationCenter.default.addObserver( - self, - selector: #selector(updateRotation), - name: UIDevice.orientationDidChangeNotification, - object: nil - ) + executeOnMain { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.updateRotation), + name: UIDevice.orientationDidChangeNotification, + object: nil + ) + } updateRotation() } @@ -123,11 +125,13 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { } deinit { - NotificationCenter.default.removeObserver( - self, - name: UIDevice.orientationDidChangeNotification, - object: nil - ) + executeOnMain { + NotificationCenter.default.removeObserver( + self, + name: UIDevice.orientationDidChangeNotification, + object: nil + ) + } } } diff --git a/Sources/StreamVideo/WebRTC/VideoCapturer.swift b/Sources/StreamVideo/WebRTC/VideoCapturer.swift index 56f7c9291..416c222f9 100644 --- a/Sources/StreamVideo/WebRTC/VideoCapturer.swift +++ b/Sources/StreamVideo/WebRTC/VideoCapturer.swift @@ -342,9 +342,9 @@ class VideoCapturer: CameraVideoCapturing { extension CMVideoDimensions { - public static var full = CMVideoDimensions(width: 1280, height: 720) - public static var half = CMVideoDimensions(width: 640, height: 480) - public static var quarter = CMVideoDimensions(width: 480, height: 360) + public static let full = CMVideoDimensions(width: 1280, height: 720) + public static let half = CMVideoDimensions(width: 640, height: 480) + public static let quarter = CMVideoDimensions(width: 480, height: 360) var area: Int32 { width * height diff --git a/Sources/StreamVideo/WebRTC/WebRTCClient.swift b/Sources/StreamVideo/WebRTC/WebRTCClient.swift index df5b076f9..5914ea1b1 100644 --- a/Sources/StreamVideo/WebRTC/WebRTCClient.swift +++ b/Sources/StreamVideo/WebRTC/WebRTCClient.swift @@ -1488,18 +1488,20 @@ class WebRTCClient: NSObject, @unchecked Sendable { }() if !isiOSAppOnMac { - NotificationCenter.default.addObserver( - self, - selector: #selector(pauseTracks), - name: UIScene.didEnterBackgroundNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(unpauseTracks), - name: UIScene.willEnterForegroundNotification, - object: nil - ) + Task { @MainActor in + NotificationCenter.default.addObserver( + self, + selector: #selector(pauseTracks), + name: UIScene.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(unpauseTracks), + name: UIScene.willEnterForegroundNotification, + object: nil + ) + } } } diff --git a/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift b/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift index 3e8471c77..b785a43b9 100644 --- a/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift +++ b/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift @@ -9,21 +9,21 @@ protocol BackgroundTaskScheduler { /// It's your responsibility to finish previously running task. /// /// Returns: `false` if system forbid background task, `true` otherwise - func beginTask(expirationHandler: (() -> Void)?) -> Bool + @MainActor func beginTask(expirationHandler: (@Sendable() -> Void)?) -> Bool func endTask() func startListeningForAppStateUpdates( - onEnteringBackground: @escaping () -> Void, + onEnteringBackground: @MainActor @escaping () -> Void, onEnteringForeground: @escaping () -> Void ) func stopListeningForAppStateUpdates() - var isAppActive: Bool { get } + @MainActor var isAppActive: Bool { get } } #if os(iOS) import UIKit -class IOSBackgroundTaskScheduler: BackgroundTaskScheduler { +class IOSBackgroundTaskScheduler: BackgroundTaskScheduler, @unchecked Sendable { private lazy var app: UIApplication? = { // We can't use `UIApplication.shared` directly because there's no way to convince the compiler // this code is accessible only for non-extension executables. @@ -50,7 +50,7 @@ class IOSBackgroundTaskScheduler: BackgroundTaskScheduler { return isActive } - func beginTask(expirationHandler: (() -> Void)?) -> Bool { + func beginTask(expirationHandler: (@Sendable() -> Void)?) -> Bool { activeBackgroundTask = app?.beginBackgroundTask { [weak self] in expirationHandler?() self?.endTask() @@ -60,54 +60,60 @@ class IOSBackgroundTaskScheduler: BackgroundTaskScheduler { func endTask() { if let activeTask = activeBackgroundTask { - app?.endBackgroundTask(activeTask) - activeBackgroundTask = nil + executeOnMain { + self.app?.endBackgroundTask(activeTask) + self.activeBackgroundTask = nil + } } } - private var onEnteringBackground: () -> Void = {} + private var onEnteringBackground: @MainActor() -> Void = {} private var onEnteringForeground: () -> Void = {} func startListeningForAppStateUpdates( - onEnteringBackground: @escaping () -> Void, + onEnteringBackground: @MainActor @escaping () -> Void, onEnteringForeground: @escaping () -> Void ) { self.onEnteringForeground = onEnteringForeground self.onEnteringBackground = onEnteringBackground - NotificationCenter.default.addObserver( - self, - selector: #selector(handleAppDidEnterBackground), - name: UIApplication.didEnterBackgroundNotification, - object: nil - ) + executeOnMain { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.handleAppDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleAppDidBecomeActive), - name: UIApplication.didBecomeActiveNotification, - object: nil - ) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.handleAppDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } } func stopListeningForAppStateUpdates() { onEnteringForeground = {} onEnteringBackground = {} - NotificationCenter.default.removeObserver( - self, - name: UIApplication.didEnterBackgroundNotification, - object: nil - ) - - NotificationCenter.default.removeObserver( - self, - name: UIApplication.didBecomeActiveNotification, - object: nil - ) + executeOnMain { + NotificationCenter.default.removeObserver( + self, + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + + NotificationCenter.default.removeObserver( + self, + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } } - @objc private func handleAppDidEnterBackground() { + @MainActor @objc private func handleAppDidEnterBackground() { onEnteringBackground() } diff --git a/Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift b/Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift index 990a13b18..2b49f219d 100644 --- a/Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift +++ b/Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift @@ -87,16 +87,18 @@ private extension DefaultConnectionRecoveryHandler { // MARK: - Event handlers -extension DefaultConnectionRecoveryHandler { +extension DefaultConnectionRecoveryHandler: @unchecked Sendable { private func appDidBecomeActive() { log.debug("App -> ✅", subsystems: .webSocket) backgroundTaskScheduler?.endTask() - reconnectIfNeeded() + Task { @MainActor in + reconnectIfNeeded() + } } - private func appDidEnterBackground() { + @MainActor private func appDidEnterBackground() { log.debug("App -> 💤", subsystems: .webSocket) guard canBeDisconnected else { @@ -132,7 +134,9 @@ extension DefaultConnectionRecoveryHandler { log.debug("Internet -> \(isAvailable ? "✅" : "❌")", subsystems: .webSocket) if isAvailable { - reconnectIfNeeded() + Task { @MainActor in + reconnectIfNeeded() + } } else { disconnectIfNeeded() } @@ -148,7 +152,9 @@ extension DefaultConnectionRecoveryHandler { case .connected: reconnectionStrategy.resetConsecutiveFailures() case .disconnected: - scheduleReconnectionTimerIfNeeded() + Task { @MainActor in + scheduleReconnectionTimerIfNeeded() + } case .initialized, .authenticating, .disconnecting: break } @@ -185,13 +191,13 @@ private extension DefaultConnectionRecoveryHandler { // MARK: - Reconnection private extension DefaultConnectionRecoveryHandler { - func reconnectIfNeeded() { + @MainActor func reconnectIfNeeded() { guard canReconnectAutomatically else { return } webSocketClient.connect() } - var canReconnectAutomatically: Bool { + @MainActor var canReconnectAutomatically: Bool { guard webSocketClient.connectionState.isAutomaticReconnectionEnabled else { log.debug("Reconnection is not required (\(webSocketClient.connectionState))", subsystems: .webSocket) return false @@ -216,13 +222,13 @@ private extension DefaultConnectionRecoveryHandler { // MARK: - Reconnection Timer private extension DefaultConnectionRecoveryHandler { - func scheduleReconnectionTimerIfNeeded() { + @MainActor func scheduleReconnectionTimerIfNeeded() { guard canReconnectAutomatically else { return } scheduleReconnectionTimer() } - func scheduleReconnectionTimer() { + @MainActor func scheduleReconnectionTimer() { let delay = reconnectionStrategy.getDelayAfterTheFailure() log.debug("Timer ⏳ \(delay) sec", subsystems: .webSocket) diff --git a/Sources/StreamVideo/WebSockets/Client/ConnectionStatus.swift b/Sources/StreamVideo/WebSockets/Client/ConnectionStatus.swift index d8dc191ea..02ff85cd4 100644 --- a/Sources/StreamVideo/WebSockets/Client/ConnectionStatus.swift +++ b/Sources/StreamVideo/WebSockets/Client/ConnectionStatus.swift @@ -7,7 +7,7 @@ import Foundation // `ConnectionStatus` is just a simplified and friendlier wrapper around `WebSocketConnectionState`. /// Describes the possible states of the client connection to the servers. -public enum ConnectionStatus: Equatable { +public enum ConnectionStatus: Equatable, Sendable { /// The client is initialized but not connected to the remote server yet. case initialized diff --git a/Sources/StreamVideo/WebSockets/Client/RetryStrategy.swift b/Sources/StreamVideo/WebSockets/Client/RetryStrategy.swift index b4717385f..14db527d0 100644 --- a/Sources/StreamVideo/WebSockets/Client/RetryStrategy.swift +++ b/Sources/StreamVideo/WebSockets/Client/RetryStrategy.swift @@ -5,7 +5,7 @@ import Foundation /// The type encapsulating the logic of computing delays for the failed actions that needs to be retried. -protocol RetryStrategy { +protocol RetryStrategy: Sendable { /// Returns the # of consecutively failed retries. var consecutiveFailuresCount: Int { get } @@ -37,7 +37,7 @@ extension RetryStrategy { } /// The default implementation of `RetryStrategy` with exponentially growing delays. -struct DefaultRetryStrategy: RetryStrategy { +struct DefaultRetryStrategy: RetryStrategy, @unchecked Sendable { static let maximumReconnectionDelay: TimeInterval = 25 @Atomic private(set) var consecutiveFailuresCount = 0 diff --git a/Sources/StreamVideo/WebSockets/Client/URLSessionWebSocketEngine.swift b/Sources/StreamVideo/WebSockets/Client/URLSessionWebSocketEngine.swift index 5d11adf0b..9562d5bec 100644 --- a/Sources/StreamVideo/WebSockets/Client/URLSessionWebSocketEngine.swift +++ b/Sources/StreamVideo/WebSockets/Client/URLSessionWebSocketEngine.swift @@ -4,7 +4,7 @@ import Foundation -class URLSessionWebSocketEngine: NSObject, WebSocketEngine { +class URLSessionWebSocketEngine: NSObject, WebSocketEngine, @unchecked Sendable { private weak var task: URLSessionWebSocketTask? { didSet { oldValue?.cancel() @@ -123,8 +123,9 @@ class URLSessionWebSocketEngine: NSObject, WebSocketEngine { private func makeURLSessionDelegateHandler() -> URLSessionDelegateHandler { let urlSessionDelegateHandler = URLSessionDelegateHandler() urlSessionDelegateHandler.onOpen = { [weak self] _ in - self?.callbackQueue.async { - self?.delegate?.webSocketDidConnect() + guard let self else { return } + self.callbackQueue.async { + self.delegate?.webSocketDidConnect() } } @@ -140,8 +141,10 @@ class URLSessionWebSocketEngine: NSObject, WebSocketEngine { log.error("WebSocket onClose", subsystems: .webSocket, error: error) } + let wsError = error self?.callbackQueue.async { [weak self] in - self?.delegate?.webSocketDidDisconnect(error: error) + guard let self else { return } + self.delegate?.webSocketDidDisconnect(error: wsError) } } @@ -167,7 +170,7 @@ class URLSessionWebSocketEngine: NSObject, WebSocketEngine { } } -class URLSessionDelegateHandler: NSObject, URLSessionDataDelegate, URLSessionWebSocketDelegate { +final class URLSessionDelegateHandler: NSObject, URLSessionDataDelegate, URLSessionWebSocketDelegate, @unchecked Sendable { var onOpen: ((_ protocol: String?) -> Void)? var onClose: ((_ code: URLSessionWebSocketTask.CloseCode, _ reason: Data?) -> Void)? var onCompletion: ((Error?) -> Void)? diff --git a/Sources/StreamVideo/WebSockets/Client/WebSocketClient.swift b/Sources/StreamVideo/WebSockets/Client/WebSocketClient.swift index 5a88e2c3e..22a42600e 100644 --- a/Sources/StreamVideo/WebSockets/Client/WebSocketClient.swift +++ b/Sources/StreamVideo/WebSockets/Client/WebSocketClient.swift @@ -4,7 +4,7 @@ import Foundation -class WebSocketClient { +class WebSocketClient: @unchecked Sendable { /// The notification center `WebSocketClient` uses to send notifications about incoming events. let eventNotificationCenter: EventNotificationCenter @@ -121,7 +121,7 @@ class WebSocketClient { /// - Parameter source: Additional information about the source of the disconnection. Default value is `.userInitiated`. func disconnect( source: WebSocketConnectionState.DisconnectionSource = .userInitiated, - completion: @escaping () -> Void + completion: @Sendable @escaping () -> Void ) { connectionState = .disconnecting(source: source) engineQueue.async { [engine, eventsBatcher] in @@ -170,7 +170,7 @@ extension WebSocketClient { } var eventBatcherBuilder: ( - _ handler: @escaping ([WrappedEvent], @escaping () -> Void) -> Void + _ handler: @escaping ([WrappedEvent], @Sendable @escaping () -> Void) -> Void ) -> EventBatcher = { Batcher(period: 0.0, handler: $0) } @@ -314,7 +314,7 @@ extension WebSocketClient { #endif extension ClientError { - public class WebSocket: ClientError {} + public class WebSocket: ClientError, @unchecked Sendable {} } /// WebSocket Error diff --git a/Sources/StreamVideo/WebSockets/Client/WebSocketEngine.swift b/Sources/StreamVideo/WebSockets/Client/WebSocketEngine.swift index db5dc9414..7bf78ddeb 100644 --- a/Sources/StreamVideo/WebSockets/Client/WebSocketEngine.swift +++ b/Sources/StreamVideo/WebSockets/Client/WebSocketEngine.swift @@ -4,7 +4,7 @@ import Foundation -protocol WebSocketEngine: AnyObject { +protocol WebSocketEngine: AnyObject, Sendable { var request: URLRequest { get } var callbackQueue: DispatchQueue { get } var delegate: WebSocketEngineDelegate? { get set } diff --git a/Sources/StreamVideo/WebSockets/Events/EventBatcher.swift b/Sources/StreamVideo/WebSockets/Events/EventBatcher.swift index 13c844d86..5b550460d 100644 --- a/Sources/StreamVideo/WebSockets/Events/EventBatcher.swift +++ b/Sources/StreamVideo/WebSockets/Events/EventBatcher.swift @@ -5,9 +5,9 @@ import Foundation /// The type that does events batching. -protocol EventBatcher { +protocol EventBatcher: Sendable { typealias Batch = [WrappedEvent] - typealias BatchHandler = (_ batch: Batch, _ completion: @escaping () -> Void) -> Void + typealias BatchHandler = (_ batch: Batch, _ completion: @Sendable @escaping () -> Void) -> Void /// The current batch of events. var currentBatch: Batch { get } @@ -22,12 +22,12 @@ protocol EventBatcher { func append(_ event: WrappedEvent) /// Ignores `period` and passes the current batch of events to handler as soon as possible. - func processImmediately(completion: @escaping () -> Void) + func processImmediately(completion: @Sendable @escaping () -> Void) } extension Batcher: EventBatcher where Item == WrappedEvent {} -final class Batcher { +final class Batcher: @unchecked Sendable { /// The batching period. If the item is added sonner then `period` has passed after the first item they will get into the same batch. private let period: TimeInterval /// The time used to create timers. @@ -35,7 +35,7 @@ final class Batcher { /// The timer that calls `processor` when fired. private var batchProcessingTimer: TimerControl? /// The closure which processes the batch. - private let handler: (_ batch: [Item], _ completion: @escaping () -> Void) -> Void + private let handler: (_ batch: [Item], _ completion: @Sendable @escaping () -> Void) -> Void /// The serial queue where item appends and batch processing is happening on. private let queue = DispatchQueue(label: "io.getstream.Batch.\(Item.self)") /// The current batch of items. @@ -44,7 +44,7 @@ final class Batcher { init( period: TimeInterval, timerType: Timer.Type = DefaultTimer.self, - handler: @escaping (_ batch: [Item], _ completion: @escaping () -> Void) -> Void + handler: @escaping (_ batch: [Item], _ completion: @Sendable @escaping () -> Void) -> Void ) { self.period = max(period, 0) self.timerType = timerType @@ -65,13 +65,13 @@ final class Batcher { } } - func processImmediately(completion: @escaping () -> Void) { + func processImmediately(completion: @Sendable @escaping () -> Void) { timerType.schedule(timeInterval: 0, queue: queue) { [weak self] in self?.process(completion: completion) } } - private func process(completion: (() -> Void)? = nil) { + private func process(completion: (@Sendable() -> Void)? = nil) { handler(currentBatch) { completion?() } currentBatch.removeAll() batchProcessingTimer?.cancel() diff --git a/Sources/StreamVideo/WebSockets/Events/EventNotificationCenter.swift b/Sources/StreamVideo/WebSockets/Events/EventNotificationCenter.swift index 31b0e0b94..d01992883 100644 --- a/Sources/StreamVideo/WebSockets/Events/EventNotificationCenter.swift +++ b/Sources/StreamVideo/WebSockets/Events/EventNotificationCenter.swift @@ -5,7 +5,7 @@ import Foundation /// The type is designed to pre-process some incoming `Event` via middlewares before being published -class EventNotificationCenter: NotificationCenter { +class EventNotificationCenter: NotificationCenter, @unchecked Sendable { private(set) var middlewares: [EventMiddleware] = [] var eventPostingQueue = DispatchQueue(label: "io.getstream.event-notification-center") @@ -18,15 +18,14 @@ class EventNotificationCenter: NotificationCenter { middlewares.append(middleware) } - func process(_ events: [WrappedEvent], postNotifications: Bool = true, completion: (() -> Void)? = nil) { + func process(_ events: [WrappedEvent], postNotifications: Bool = true, completion: (@Sendable() -> Void)? = nil) { let processingEventsDebugMessage: () -> String = { let eventNames = events.map(\.name) return "Processing webSocket events: \(eventNames)/" } log.debug(processingEventsDebugMessage(), subsystems: .webSocket) - var eventsToPost = [Event]() - eventsToPost = events.compactMap { + let eventsToPost = events.compactMap { self.middlewares.process(event: $0) } @@ -43,7 +42,7 @@ class EventNotificationCenter: NotificationCenter { } extension EventNotificationCenter { - func process(_ event: WrappedEvent, postNotification: Bool = true, completion: (() -> Void)? = nil) { + func process(_ event: WrappedEvent, postNotification: Bool = true, completion: (@Sendable() -> Void)? = nil) { process([event], postNotifications: postNotification, completion: completion) } } diff --git a/Sources/StreamVideo/WebSockets/Events/JsonEventDecoder.swift b/Sources/StreamVideo/WebSockets/Events/JsonEventDecoder.swift index 0e8827a4f..8c21e9b73 100644 --- a/Sources/StreamVideo/WebSockets/Events/JsonEventDecoder.swift +++ b/Sources/StreamVideo/WebSockets/Events/JsonEventDecoder.swift @@ -26,11 +26,11 @@ extension UserResponse { } extension ClientError { - public class UnsupportedEventType: ClientError { + public final class UnsupportedEventType: ClientError { override public var localizedDescription: String { "The incoming event type is not supported. Ignoring." } } - public class EventDecoding: ClientError { + public final class EventDecoding: ClientError { override init(_ message: String, _ file: StaticString = #file, _ line: UInt = #line) { super.init(message, file, line) } diff --git a/Sources/StreamVideo/WebSockets/Events/StreamJsonDecoder.swift b/Sources/StreamVideo/WebSockets/Events/StreamJsonDecoder.swift index 683a6cfe2..66ec8fa3e 100644 --- a/Sources/StreamVideo/WebSockets/Events/StreamJsonDecoder.swift +++ b/Sources/StreamVideo/WebSockets/Events/StreamJsonDecoder.swift @@ -6,7 +6,7 @@ import Foundation // MARK: - JSONDecoder Stream -final class StreamJSONDecoder: JSONDecoder { +final class StreamJSONDecoder: JSONDecoder, @unchecked Sendable { let iso8601formatter: ISO8601DateFormatter let dateCache: NSCache @@ -52,7 +52,7 @@ final class StreamJSONDecoder: JSONDecoder { extension JSONDecoder { /// A default `JSONDecoder`. - static var `default`: JSONDecoder = stream + static let `default`: JSONDecoder = stream /// A Stream Chat JSON decoder. static let stream: StreamJSONDecoder = { @@ -64,9 +64,9 @@ extension JSONDecoder { extension JSONEncoder { /// A default `JSONEncoder`. - static var `default`: JSONEncoder = stream + static let `default`: JSONEncoder = stream /// A default gzip `JSONEncoder`. - static var defaultGzip: JSONEncoder = streamGzip + static let defaultGzip: JSONEncoder = streamGzip /// A Stream Chat JSON encoder. static let stream: JSONEncoder = { diff --git a/Sources/StreamVideo/protobuf/sfu/event/events.pb.swift b/Sources/StreamVideo/protobuf/sfu/event/events.pb.swift index d5ffb15fc..edccbb642 100644 --- a/Sources/StreamVideo/protobuf/sfu/event/events.pb.swift +++ b/Sources/StreamVideo/protobuf/sfu/event/events.pb.swift @@ -1036,7 +1036,7 @@ struct Stream_Video_Sfu_Event_VideoLayerSetting { extension Stream_Video_Sfu_Event_VideoLayerSetting.Priority: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Event_VideoLayerSetting.Priority] = [ + static let allCases: [Stream_Video_Sfu_Event_VideoLayerSetting.Priority] = [ .highUnspecified, .low, .medium, @@ -1881,7 +1881,7 @@ extension Stream_Video_Sfu_Event_TrackPublished: SwiftProtobuf.Message, SwiftPro var _type: Stream_Video_Sfu_Models_TrackType = .unspecified var _participant: Stream_Video_Sfu_Models_Participant? = nil - static let defaultInstance = _StorageClass() + nonisolated(unsafe) static let defaultInstance = _StorageClass() private init() {} @@ -1975,7 +1975,7 @@ extension Stream_Video_Sfu_Event_TrackUnpublished: SwiftProtobuf.Message, SwiftP var _cause: Stream_Video_Sfu_Models_TrackUnpublishReason = .unspecified var _participant: Stream_Video_Sfu_Models_Participant? = nil - static let defaultInstance = _StorageClass() + nonisolated(unsafe) static let defaultInstance = _StorageClass() private init() {} @@ -2077,7 +2077,7 @@ extension Stream_Video_Sfu_Event_JoinRequest: SwiftProtobuf.Message, SwiftProtob var _migration: Stream_Video_Sfu_Event_Migration? = nil var _fastReconnect: Bool = false - static let defaultInstance = _StorageClass() + nonisolated(unsafe) static let defaultInstance = _StorageClass() private init() {} diff --git a/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift b/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift index 8df3dfc17..f35f99bae 100644 --- a/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift +++ b/Sources/StreamVideo/protobuf/sfu/models/models.pb.swift @@ -54,7 +54,7 @@ enum Stream_Video_Sfu_Models_PeerType: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_PeerType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_PeerType] = [ + static let allCases: [Stream_Video_Sfu_Models_PeerType] = [ .publisherUnspecified, .subscriber, ] @@ -100,7 +100,7 @@ enum Stream_Video_Sfu_Models_ConnectionQuality: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_ConnectionQuality: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_ConnectionQuality] = [ + static let allCases: [Stream_Video_Sfu_Models_ConnectionQuality] = [ .unspecified, .poor, .good, @@ -148,7 +148,7 @@ enum Stream_Video_Sfu_Models_VideoQuality: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_VideoQuality: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_VideoQuality] = [ + static let allCases: [Stream_Video_Sfu_Models_VideoQuality] = [ .lowUnspecified, .mid, .high, @@ -199,7 +199,7 @@ enum Stream_Video_Sfu_Models_TrackType: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_TrackType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_TrackType] = [ + static let allCases: [Stream_Video_Sfu_Models_TrackType] = [ .unspecified, .audio, .video, @@ -296,7 +296,7 @@ enum Stream_Video_Sfu_Models_ErrorCode: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_ErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_ErrorCode] = [ + static let allCases: [Stream_Video_Sfu_Models_ErrorCode] = [ .unspecified, .publishTrackNotFound, .publishTracksMismatch, @@ -372,7 +372,7 @@ enum Stream_Video_Sfu_Models_SdkType: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_SdkType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_SdkType] = [ + static let allCases: [Stream_Video_Sfu_Models_SdkType] = [ .unspecified, .react, .angular, @@ -438,7 +438,7 @@ enum Stream_Video_Sfu_Models_TrackUnpublishReason: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_TrackUnpublishReason: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_TrackUnpublishReason] = [ + static let allCases: [Stream_Video_Sfu_Models_TrackUnpublishReason] = [ .unspecified, .userMuted, .permissionRevoked, @@ -485,7 +485,7 @@ enum Stream_Video_Sfu_Models_GoAwayReason: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_GoAwayReason: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_GoAwayReason] = [ + static let allCases: [Stream_Video_Sfu_Models_GoAwayReason] = [ .unspecified, .shuttingDown, .rebalance, @@ -533,7 +533,7 @@ enum Stream_Video_Sfu_Models_CallEndedReason: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_CallEndedReason: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_CallEndedReason] = [ + static let allCases: [Stream_Video_Sfu_Models_CallEndedReason] = [ .unspecified, .ended, .liveEnded, @@ -601,7 +601,7 @@ enum Stream_Video_Sfu_Models_WebsocketReconnectStrategy: SwiftProtobuf.Enum { extension Stream_Video_Sfu_Models_WebsocketReconnectStrategy: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Stream_Video_Sfu_Models_WebsocketReconnectStrategy] = [ + static let allCases: [Stream_Video_Sfu_Models_WebsocketReconnectStrategy] = [ .unspecified, .disconnect, .fast, diff --git a/Sources/StreamVideoSwiftUI/Appearance.swift b/Sources/StreamVideoSwiftUI/Appearance.swift index 67f33b364..9ee3c7802 100644 --- a/Sources/StreamVideoSwiftUI/Appearance.swift +++ b/Sources/StreamVideoSwiftUI/Appearance.swift @@ -25,7 +25,7 @@ public class Appearance { } /// Provider for custom localization which is dependent on App Bundle. - public static var localizationProvider: (_ key: String, _ table: String) -> String = { key, table in + nonisolated(unsafe) public static var localizationProvider: (_ key: String, _ table: String) -> String = { key, table in Bundle.streamVideoUI.localizedString(forKey: key, value: nil, table: table) } } @@ -33,12 +33,12 @@ public class Appearance { // MARK: - Appearance + Default public extension Appearance { - static var `default`: Appearance = .init() + nonisolated(unsafe) static var `default`: Appearance = .init() } /// Provides the default value of the `Appearance` class. enum AppearanceKey: InjectionKey { - static var currentValue: Appearance = Appearance() + nonisolated(unsafe) static var currentValue: Appearance = Appearance() } extension InjectedValues { diff --git a/Sources/StreamVideoSwiftUI/CallView/Participants/CallParticipantMenuAction.swift b/Sources/StreamVideoSwiftUI/CallView/Participants/CallParticipantMenuAction.swift index f40f8647a..a0b29bba7 100644 --- a/Sources/StreamVideoSwiftUI/CallView/Participants/CallParticipantMenuAction.swift +++ b/Sources/StreamVideoSwiftUI/CallView/Participants/CallParticipantMenuAction.swift @@ -16,7 +16,7 @@ public struct CallParticipantMenuAction: Identifiable { /// The name of the icon associated with the action. public var iconName: String /// The closure to execute when the action is triggered, passing the participant's ID. - public var action: (String) -> Void + public var action: @MainActor(String) -> Void /// Optional confirmation popup data that may be presented before executing the action. public var confirmationPopup: ConfirmationPopup? /// A flag indicating whether the action is destructive (e.g., delete). diff --git a/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift b/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift index ddd4a3c68..6ff5be9b5 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift @@ -32,7 +32,7 @@ public struct ParticipantsGridLayout: View { public var body: some View { ZStack { - if orientationAdapter.orientation.isPortrait { + if orientationAdapter.orientation?.isPortrait == true { VideoParticipantsViewPortrait( viewFactory: viewFactory, call: call, diff --git a/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift b/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift index 2f84335dc..5f7d6fd0b 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift @@ -54,7 +54,7 @@ public struct ScreenSharingView: View { public var body: some View { VStack(spacing: innerItemSpace) { - if !viewModel.hideUIElements, orientationAdapter.orientation.isPortrait || UIDevice.current.isIpad { + if !viewModel.hideUIElements, orientationAdapter.orientation?.isPortrait == true || UIDevice.current.isIpad { Text("\(screenSharing.participant.name) presenting") .foregroundColor(colors.text) .padding() diff --git a/Sources/StreamVideoSwiftUI/CallView/VideoRenderer.swift b/Sources/StreamVideoSwiftUI/CallView/VideoRenderer.swift index 0e47a2be2..d547e977e 100644 --- a/Sources/StreamVideoSwiftUI/CallView/VideoRenderer.swift +++ b/Sources/StreamVideoSwiftUI/CallView/VideoRenderer.swift @@ -110,11 +110,11 @@ public class VideoRenderer: RTCMTLVideoView { let queue = DispatchQueue(label: "video-track") /// The associated RTCVideoTrack being rendered. - weak var track: RTCVideoTrack? + nonisolated(unsafe) weak var track: RTCVideoTrack? /// Unique identifier for the video renderer instance. private let identifier = UUID() - private var cancellable: AnyCancellable? + nonisolated(unsafe) private var cancellable: AnyCancellable? /// Preferred frames per second for rendering. private(set) var preferredFramesPerSecond: Int = UIScreen.main.maximumFramesPerSecond { @@ -162,8 +162,12 @@ public class VideoRenderer: RTCMTLVideoView { /// Cleans up resources when the VideoRenderer instance is deallocated. deinit { - cancellable?.cancel() log.debug("\(type(of: self)):\(identifier) deallocating", subsystems: .webRTC) + cleanUp() + } + + nonisolated func cleanUp() { + cancellable?.cancel() track?.remove(self) } @@ -215,7 +219,7 @@ extension VideoRenderer { subsystems: .webRTC ) add(track: track) - DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + 0.01) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { [weak self] in guard let self else { return } let prev = participant.trackSize if let viewSize, prev != viewSize { diff --git a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/CallEndedViewModifier.swift b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/CallEndedViewModifier.swift index d817297fe..8c6e743d8 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/CallEndedViewModifier.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/CallEndedViewModifier.swift @@ -7,6 +7,7 @@ import Foundation import StreamVideo import SwiftUI +@MainActor private final class CallEndedViewModifierViewModel: ObservableObject { @Injected(\.streamVideo) private var streamVideo @@ -124,7 +125,7 @@ private struct CallEndedViewModifier: ViewModifier { } @available(iOS, introduced: 13, obsoleted: 14) -private struct CallEndedViewModifier_iOS13: ViewModifier { +private struct CallEndedViewModifier_iOS13: ViewModifier, @unchecked Sendable { private var presentationValidator: (Call?) -> Bool private var subviewProvider: (Call?, @escaping () -> Void) -> Subview @@ -211,7 +212,7 @@ extension View { /// - content: A viewBuilder that returns the modal's content. The viewModifier /// will provide a dismiss closure that can be called from the content to close the modal. @ViewBuilder - public func onCallEnded( + @MainActor public func onCallEnded( presentationValidator: @escaping (Call?) -> Bool = { _ in true }, @ViewBuilder _ content: @escaping (Call?, @escaping () -> Void) -> some View ) -> some View { diff --git a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/ParticipantEventViewModifier.swift b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/ParticipantEventViewModifier.swift index 5554b95da..9d2400965 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/ParticipantEventViewModifier.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/ParticipantEventViewModifier.swift @@ -35,7 +35,7 @@ struct ParticipantEventsNotificationViewModifier: ViewModifier { extension View { /// A viewModifier that displays a notification when a participant event(join or left) occurs. - @ViewBuilder + @MainActor @ViewBuilder public func presentParticipantEventsNotification( viewModel: CallViewModel ) -> some View { diff --git a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/Snapshot/SnapshotViewModifier.swift b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/Snapshot/SnapshotViewModifier.swift index bcf01de1c..a15d1d283 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/Snapshot/SnapshotViewModifier.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/Snapshot/SnapshotViewModifier.swift @@ -15,7 +15,7 @@ struct SnapshotViewContainer: UIViewRepresentable { /// A coordinator class that manages snapshot triggering and handling within the /// `SnapshotViewContainer`. - final class SnapshotViewContainerCoordinator { + @MainActor final class SnapshotViewContainerCoordinator { private var trigger: SnapshotTriggering private let snapshotHandler: (UIImage) -> Void private var cancellable: AnyCancellable? diff --git a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift index 7aec6223e..2b0e84ded 100644 --- a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift +++ b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift @@ -6,12 +6,13 @@ import Combine import SwiftUI /// A property wrapper type that instantiates an observable object. -@propertyWrapper @available(iOS, introduced: 13, obsoleted: 14) +@preconcurrency @propertyWrapper @available(iOS, introduced: 13, obsoleted: 14) +@MainActor public struct BackportStateObject: DynamicProperty - where ObjectType.ObjectWillChangePublisher == ObservableObjectPublisher { + where ObjectType: Sendable, ObjectType.ObjectWillChangePublisher == ObservableObjectPublisher { /// Wrapper that helps with initialising without actually having an ObservableObject yet - private class ObservedObjectWrapper: ObservableObject { + @MainActor private class ObservedObjectWrapper: ObservableObject { @PublishedObject var wrappedObject: ObjectType? = nil init() {} } @@ -51,7 +52,8 @@ public struct BackportStateObject: DynamicProperty /// Just like @Published this sends willSet events to the enclosing ObservableObject's ObjectWillChangePublisher /// but unlike @Published it also sends the wrapped value's published changes on to the enclosing ObservableObject @propertyWrapper @available(iOS, introduced: 13, obsoleted: 14) -public struct PublishedObject { +@MainActor +public struct PublishedObject: @unchecked Sendable { public init(wrappedValue: Value) where Value: ObservableObject, Value.ObjectWillChangePublisher == ObservableObjectPublisher { self.wrappedValue = wrappedValue diff --git a/Sources/StreamVideoSwiftUI/Utils.swift b/Sources/StreamVideoSwiftUI/Utils.swift index 6caa299c7..4d1a5296e 100644 --- a/Sources/StreamVideoSwiftUI/Utils.swift +++ b/Sources/StreamVideoSwiftUI/Utils.swift @@ -22,7 +22,7 @@ public class Utils { /// Provides the default value of the `Utils` class. public enum UtilsKey: InjectionKey { - public static var currentValue: Utils = .init() + nonisolated(unsafe) public static var currentValue: Utils = .init() } extension InjectedValues { diff --git a/Sources/StreamVideoSwiftUI/Utils/Camera.swift b/Sources/StreamVideoSwiftUI/Utils/Camera.swift index 6c75cf9d8..11a44ee3a 100644 --- a/Sources/StreamVideoSwiftUI/Utils/Camera.swift +++ b/Sources/StreamVideoSwiftUI/Utils/Camera.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // -import AVFoundation +@preconcurrency import AVFoundation import CoreImage import os.log import UIKit @@ -264,7 +264,7 @@ class Camera: NSObject, @unchecked Sendable { } } - private var deviceOrientation: UIDeviceOrientation { + @MainActor private var deviceOrientation: UIDeviceOrientation { UIScreen.main.orientation } @@ -289,14 +289,16 @@ extension Camera: AVCaptureVideoDataOutputSampleBufferDelegate { ) { guard let pixelBuffer = sampleBuffer.imageBuffer else { return } - if - connection.isVideoOrientationSupported, - let videoOrientation = videoOrientationFor(deviceOrientation), - connection.videoOrientation != videoOrientation { - connection.videoOrientation = videoOrientation - } + Task { @MainActor in + if + connection.isVideoOrientationSupported, + let videoOrientation = videoOrientationFor(deviceOrientation), + connection.videoOrientation != videoOrientation { + connection.videoOrientation = videoOrientation + } - addToPreviewStream?(CIImage(cvPixelBuffer: pixelBuffer)) + addToPreviewStream?(CIImage(cvPixelBuffer: pixelBuffer)) + } } } @@ -320,3 +322,6 @@ private extension UIScreen { @available(iOS 14.0, *) private let logger = Logger(subsystem: "com.apple.swiftplaygroundscontent.capturingphotos", category: "Camera") + +@available(iOS 14.0, *) +extension Logger: @unchecked Sendable {} diff --git a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift index 2b40d35b9..dded5a946 100644 --- a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift +++ b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // -import Combine +@preconcurrency import Combine import Foundation import StreamVideo #if canImport(UIKit) @@ -21,11 +21,11 @@ public enum StreamDeviceOrientation: Equatable { } /// An observable object that adapts to device orientation changes. -open class StreamDeviceOrientationAdapter: ObservableObject { - public typealias Provider = () -> StreamDeviceOrientation +public class StreamDeviceOrientationAdapter: ObservableObject, @unchecked Sendable { + public typealias Provider = @MainActor @Sendable() -> StreamDeviceOrientation /// The default provider for device orientation based on platform. - public static let defaultProvider: Provider = { + @MainActor public static let defaultProvider: Provider = { #if canImport(UIKit) switch UIDevice.current.orientation { case .unknown, .portrait, .portraitUpsideDown: @@ -42,11 +42,11 @@ open class StreamDeviceOrientationAdapter: ObservableObject { #endif } - private var provider: Provider + @MainActor private lazy var provider: Provider = StreamDeviceOrientationAdapter.defaultProvider private var notificationCancellable: AnyCancellable? /// The current orientation observed by the adapter. - @Published public private(set) var orientation: StreamDeviceOrientation + @Published public private(set) var orientation: StreamDeviceOrientation? /// Initializes an adapter for observing device orientation changes. /// - Parameters: @@ -54,32 +54,40 @@ open class StreamDeviceOrientationAdapter: ObservableObject { /// - provider: A custom provider for determining device orientation. public init( notificationCenter: NotificationCenter = .default, - _ provider: @escaping Provider = StreamDeviceOrientationAdapter.defaultProvider + _ provider: Provider? = nil ) { - self.provider = provider - orientation = provider() - - #if canImport(UIKit) - // Subscribe to orientation change notifications on UIKit platforms. - notificationCancellable = notificationCenter - .publisher(for: UIDevice.orientationDidChangeNotification) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.orientation = provider() // Update orientation based on the provider. + Task { @MainActor in + if let provider { + self.provider = provider } - #endif + orientation = self.provider() + + #if canImport(UIKit) + // Subscribe to orientation change notifications on UIKit platforms. + notificationCancellable = notificationCenter + .publisher(for: UIDevice.orientationDidChangeNotification) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.orientation = self.provider() // Update orientation based on the provider. + } + #endif + } } /// Cleans up resources when the adapter is deallocated. deinit { + cleanup() + } + + private func cleanup() { notificationCancellable?.cancel() // Cancel notification subscription. } } /// Provides the default value of the `StreamPictureInPictureAdapter` class. enum StreamDeviceOrientationAdapterKey: InjectionKey { - static var currentValue: StreamDeviceOrientationAdapter = .init() + nonisolated(unsafe) static var currentValue: StreamDeviceOrientationAdapter = .init() } extension InjectedValues { diff --git a/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift b/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift index 0756adedb..51139388a 100644 --- a/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift +++ b/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift @@ -13,7 +13,7 @@ public class Formatters { /// Provides the default value of the `Formatters` class. enum FormattersKey: InjectionKey { - static var currentValue: Formatters = .init() + @MainActor static var currentValue: Formatters = .init() } extension InjectedValues { diff --git a/Sources/StreamVideoSwiftUI/Utils/HelperViews.swift b/Sources/StreamVideoSwiftUI/Utils/HelperViews.swift index d9d3341c4..eea96484a 100644 --- a/Sources/StreamVideoSwiftUI/Utils/HelperViews.swift +++ b/Sources/StreamVideoSwiftUI/Utils/HelperViews.swift @@ -43,7 +43,7 @@ public struct CallIconView: View { } } -public struct CallIconStyle { +public struct CallIconStyle: Sendable { public let backgroundColor: Color public let foregroundColor: Color public let opacity: CGFloat diff --git a/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift b/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift index 972576281..aa1b4ab76 100644 --- a/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift +++ b/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift @@ -10,12 +10,11 @@ import UIKit public protocol KeyboardReadable { var keyboardWillChangePublisher: AnyPublisher { get } var keyboardDidChangePublisher: AnyPublisher { get } - var keyboardHeight: AnyPublisher { get } } /// Default implementation. extension KeyboardReadable { - public var keyboardWillChangePublisher: AnyPublisher { + @MainActor public var keyboardWillChangePublisher: AnyPublisher { Publishers.Merge( NotificationCenter.default .publisher(for: UIResponder.keyboardWillShowNotification) @@ -28,7 +27,7 @@ extension KeyboardReadable { .eraseToAnyPublisher() } - public var keyboardDidChangePublisher: AnyPublisher { + @MainActor public var keyboardDidChangePublisher: AnyPublisher { Publishers.Merge( NotificationCenter.default .publisher(for: UIResponder.keyboardDidShowNotification) @@ -40,23 +39,6 @@ extension KeyboardReadable { ) .eraseToAnyPublisher() } - - public var keyboardHeight: AnyPublisher { - NotificationCenter - .default - .publisher(for: UIResponder.keyboardDidShowNotification) - .map { notification in - if let keyboardFrame: NSValue = notification - .userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { - let keyboardRectangle = keyboardFrame.cgRectValue - let keyboardHeight = keyboardRectangle.height - return keyboardHeight - } else { - return 0 - } - } - .eraseToAnyPublisher() - } } /// View modifier for hiding the keyboard on tap. @@ -81,7 +63,7 @@ public struct HideKeyboardOnTapGesture: ViewModifier { } /// Resigns first responder and hides the keyboard. -public func resignFirstResponder() { +@MainActor public func resignFirstResponder() { UIApplication.shared.sendAction( #selector(UIResponder.resignFirstResponder), to: nil, diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift index 25d8fb20d..2c07d92bb 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift @@ -26,8 +26,8 @@ protocol StreamAVPictureInPictureViewControlling { } @available(iOS 15.0, *) -final class StreamAVPictureInPictureVideoCallViewController: AVPictureInPictureVideoCallViewController, - StreamAVPictureInPictureViewControlling { +final class StreamAVPictureInPictureVideoCallViewController: + AVPictureInPictureVideoCallViewController, StreamAVPictureInPictureViewControlling { private let contentView: StreamPictureInPictureVideoRenderer = .init() diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift index 7b4a62f25..6bc5e29cd 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift @@ -10,6 +10,7 @@ import UIKit /// This class encapsulates the logic for managing picture-in-picture functionality during a video call. It tracks /// changes in the call, updates related to call participants, and changes in the source view for Picture in /// Picture display. +@MainActor final class StreamPictureInPictureAdapter { /// The active call. @@ -47,7 +48,7 @@ final class StreamPictureInPictureAdapter { private var participantUpdatesCancellable: AnyCancellable? /// The actual picture-in-picture controller. - private lazy var pictureInPictureController = StreamPictureInPictureController() + @MainActor private lazy var pictureInPictureController = StreamPictureInPictureController() // MARK: - Private Helpers @@ -94,7 +95,7 @@ final class StreamPictureInPictureAdapter { /// Provides the default value of the `StreamPictureInPictureAdapter` class. enum StreamPictureInPictureAdapterKey: InjectionKey { - static var currentValue: StreamPictureInPictureAdapter = .init() + @MainActor static var currentValue: StreamPictureInPictureAdapter = .init() } extension InjectedValues { diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureController.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureController.swift index c2d28aefe..f3e83ef76 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureController.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureController.swift @@ -70,7 +70,7 @@ final class StreamPictureInPictureController: NSObject, AVPictureInPictureContro /// background. /// /// - Returns `nil` if AVPictureInPictureController is not supported, or the controller otherwise. - init?(canStartPictureInPictureAutomaticallyFromInline: Bool = true) { + @MainActor init?(canStartPictureInPictureAutomaticallyFromInline: Bool = true) { guard AVPictureInPictureController.isPictureInPictureSupported() else { return nil } @@ -189,7 +189,7 @@ final class StreamPictureInPictureController: NSObject, AVPictureInPictureContro trackStateAdapter.isEnabled = isActive } - private func subscribeToApplicationStateNotifications() { + @MainActor private func subscribeToApplicationStateNotifications() { #if canImport(UIKit) /// If we are running on a UIKit application, we observe the application state in order to disable /// PictureInPicture when active but the app is in foreground. diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamYUVToARGBConversion.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamYUVToARGBConversion.swift index f1a1cd327..a99abdf4f 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamYUVToARGBConversion.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamYUVToARGBConversion.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // -import Accelerate +@preconcurrency import Accelerate import Foundation /// A class dedicated to converting YUV (YpCbCr) image data to ARGB format. diff --git a/Sources/StreamVideoSwiftUI/Utils/StreamPixelBufferRepository/StreamPixelBufferRepository.swift b/Sources/StreamVideoSwiftUI/Utils/StreamPixelBufferRepository/StreamPixelBufferRepository.swift index 56efd58bf..838440ced 100644 --- a/Sources/StreamVideoSwiftUI/Utils/StreamPixelBufferRepository/StreamPixelBufferRepository.swift +++ b/Sources/StreamVideoSwiftUI/Utils/StreamPixelBufferRepository/StreamPixelBufferRepository.swift @@ -66,7 +66,7 @@ final class StreamPixelBufferRepository { } extension StreamPixelBufferRepository: InjectionKey { - static var currentValue: StreamPixelBufferRepository = .init() + nonisolated(unsafe) static var currentValue: StreamPixelBufferRepository = .init() } extension InjectedValues { diff --git a/Sources/StreamVideoSwiftUI/Utils/ToastView.swift b/Sources/StreamVideoSwiftUI/Utils/ToastView.swift index 2479315d3..40f8a958d 100644 --- a/Sources/StreamVideoSwiftUI/Utils/ToastView.swift +++ b/Sources/StreamVideoSwiftUI/Utils/ToastView.swift @@ -56,6 +56,7 @@ public struct ToastView: View { } } +@MainActor public struct ToastModifier: ViewModifier { @Binding var toast: Toast? @@ -128,7 +129,7 @@ public struct ToastModifier: ViewModifier { } extension View { - public func toastView(toast: Binding) -> some View { + @MainActor public func toastView(toast: Binding) -> some View { modifier(ToastModifier(toast: toast)) } } diff --git a/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift b/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift index cafcedd4b..4cce743f8 100644 --- a/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift +++ b/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift @@ -16,7 +16,7 @@ final class VideoRendererPool { /// Initializes the `VideoRendererPool` with a specified initial capacity. /// /// - Parameter initialCapacity: The initial capacity of the pool (default is 2). - init(initialCapacity: Int = 2) { + @MainActor init(initialCapacity: Int = 2) { // Initialize the pool with a capacity and a factory closure to create `VideoRenderer` instances pool = ReusePool(initialCapacity: initialCapacity) { VideoRenderer(frame: CGRect(origin: .zero, size: .zero)) @@ -33,7 +33,7 @@ final class VideoRendererPool { /// /// - Parameter size: The desired size for the acquired `VideoRenderer`. /// - Returns: A `VideoRenderer` instance from the pool. - func acquireRenderer(size: CGSize) -> VideoRenderer { + @MainActor func acquireRenderer(size: CGSize) -> VideoRenderer { let renderer = pool.acquire() renderer.frame.size = size // Set the size of the renderer return renderer @@ -48,7 +48,7 @@ final class VideoRendererPool { } extension VideoRendererPool: InjectionKey { - static var currentValue: VideoRendererPool = .init() + @MainActor static var currentValue: VideoRendererPool = .init() } extension InjectedValues { diff --git a/Sources/StreamVideoUIKit/Utils/Animation.swift b/Sources/StreamVideoUIKit/Utils/Animation.swift index 2b25ed671..1d2444b32 100644 --- a/Sources/StreamVideoUIKit/Utils/Animation.swift +++ b/Sources/StreamVideoUIKit/Utils/Animation.swift @@ -4,7 +4,7 @@ import UIKit -public func Animate( +@MainActor public func Animate( duration: TimeInterval = 0.25, delay: TimeInterval = 0, _ actions: @escaping () -> Void, @@ -19,7 +19,7 @@ public func Animate( ) } -func Animate( +@MainActor func Animate( duration: TimeInterval = 0.25, delay: TimeInterval = 0, isAnimated: Bool = true, diff --git a/Sources/StreamVideoUIKit/Utils/UIView+Extensions.swift b/Sources/StreamVideoUIKit/Utils/UIView+Extensions.swift index c97dff9d5..c903cae9a 100644 --- a/Sources/StreamVideoUIKit/Utils/UIView+Extensions.swift +++ b/Sources/StreamVideoUIKit/Utils/UIView+Extensions.swift @@ -125,6 +125,7 @@ public enum LayoutAnchorName { case trailing case width + @MainActor func makeConstraint(fromView: UIView, toView: UIView, constant: CGFloat = 0) -> NSLayoutConstraint { switch self { case .bottom: @@ -154,6 +155,7 @@ public enum LayoutAnchorName { } } + @MainActor func makeConstraint(fromView: UIView, toLayoutGuide: UILayoutGuide, constant: CGFloat = 0) -> NSLayoutConstraint? { switch self { case .bottom: @@ -182,6 +184,7 @@ public enum LayoutAnchorName { } } + @MainActor func makeConstraint(fromView: UIView, constant: CGFloat) -> NSLayoutConstraint? { switch self { case .height: diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 51639c6a5..baf2c9b51 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -6108,7 +6108,8 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG STREAM_TESTS"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Test; @@ -6146,7 +6147,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -6204,7 +6205,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -6263,6 +6264,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -6335,7 +6337,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -6642,7 +6644,8 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG STREAM_TESTS"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -6701,7 +6704,8 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; }; @@ -6751,7 +6755,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -6801,7 +6805,7 @@ PROVISIONING_PROFILE_SPECIFIER = VideoDemoAppStore; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.getstream.iOS.VideoDemoApp"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -7011,7 +7015,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -7050,7 +7054,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -7126,7 +7130,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -7164,7 +7168,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_STRICT_CONCURRENCY = targeted; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -7243,6 +7247,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -7281,6 +7286,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; From d7dc3951f6e3186f4348f5f34c21dec0e32c47f5 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 3 Jul 2024 17:49:11 +0200 Subject: [PATCH 02/21] Compiling on both Xcodes --- DemoApp/Sources/AppDelegate.swift | 8 +++++++- .../Components/Chat/DemoChatViewFactory.swift | 8 +++++++- .../Sources/Components/CodeScanner/CodeScanner.swift | 12 +++++++++--- .../Components/Reactions/ReactionsAdapter.swift | 6 ++++++ .../Snapshot/LocalParticipantSnapshotViewModel.swift | 9 +++++++-- .../Components/Snapshot/SnapshotTrigger.swift | 7 +++++++ .../CallKit/CallKitPushNotificationAdapter.swift | 8 +++++++- Sources/StreamVideo/WebRTC/AudioSession.swift | 8 ++++---- .../CallingViews/iOS13/BackportStateObject.swift | 8 +++++++- .../Utils/Formatters/Formatters.swift | 6 ++++++ .../StreamVideoSwiftUI/Utils/KeyboardReadable.swift | 8 ++++---- ...amAVPictureInPictureVideoCallViewController.swift | 10 +++++++++- .../StreamPictureInPictureAdapter.swift | 6 ++++++ .../StreamPictureInPictureVideoRenderer.swift | 8 +++++++- .../Utils/VideoRendererPool/VideoRendererPool.swift | 6 ++++++ 15 files changed, 99 insertions(+), 19 deletions(-) diff --git a/DemoApp/Sources/AppDelegate.swift b/DemoApp/Sources/AppDelegate.swift index ca5c2ff89..f67d13ef6 100644 --- a/DemoApp/Sources/AppDelegate.swift +++ b/DemoApp/Sources/AppDelegate.swift @@ -7,7 +7,7 @@ import StreamVideo import SwiftUI import UIKit -class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { +class AppDelegate: NSObject, UIApplicationDelegate { @Injected(\.streamVideo) var streamVideo @@ -118,3 +118,9 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele } } } + +#if swift(>=6.0) +extension AppDelegate: @preconcurrency UNUserNotificationCenterDelegate {} +#else +extension AppDelegate: UNUserNotificationCenterDelegate {} +#endif diff --git a/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift b/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift index 55aa0dad1..26b8e99a7 100644 --- a/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift +++ b/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift @@ -9,7 +9,7 @@ import class StreamVideoSwiftUI.CallViewModel import SwiftUI @MainActor -final class DemoChatViewFactory: ViewFactory { +final class DemoChatViewFactory { @Injected(\.chatClient) var chatClient: ChatClient @@ -34,3 +34,9 @@ final class DemoChatViewFactory: ViewFactory { ) } } + +#if swift(>=6.0) +extension DemoChatViewFactory: @preconcurrency ViewFactory {} +#else +extension DemoChatViewFactory: ViewFactory {} +#endif diff --git a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift index fb12c5293..2c9f1e259 100644 --- a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift +++ b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift @@ -112,8 +112,7 @@ struct CodeScannerView_Previews: PreviewProvider { } } -final class ScannerViewController: UIViewController, UINavigationControllerDelegate, AVCaptureMetadataOutputObjectsDelegate, - UIAdaptivePresentationControllerDelegate { +final class ScannerViewController: UIViewController, UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate { private let photoOutput = AVCapturePhotoOutput() private var isCapturing = false private var handler: ((UIImage) -> Void)? @@ -487,7 +486,14 @@ final class ScannerViewController: UIViewController, UINavigationControllerDeleg } } -extension ScannerViewController: AVCapturePhotoCaptureDelegate { +#if swift(>=6.0) +extension ScannerViewController: @preconcurrency AVCapturePhotoCaptureDelegate, + @preconcurrency AVCaptureMetadataOutputObjectsDelegate {} +#else +extension ScannerViewController: AVCapturePhotoCaptureDelegate, AVCaptureMetadataOutputObjectsDelegate {} +#endif + +extension ScannerViewController { func photoOutput( _ output: AVCapturePhotoOutput, diff --git a/DemoApp/Sources/Components/Reactions/ReactionsAdapter.swift b/DemoApp/Sources/Components/Reactions/ReactionsAdapter.swift index d05d0adfc..7f55e5223 100644 --- a/DemoApp/Sources/Components/Reactions/ReactionsAdapter.swift +++ b/DemoApp/Sources/Components/Reactions/ReactionsAdapter.swift @@ -214,9 +214,15 @@ final class ReactionsAdapter: ObservableObject { } } +#if swift(>=6.0) +extension ReactionsAdapter: @preconcurrency InjectionKey { + static var currentValue: ReactionsAdapter = .init() +} +#else extension ReactionsAdapter: InjectionKey { static var currentValue: ReactionsAdapter = .init() } +#endif extension InjectedValues { var reactionsAdapter: ReactionsAdapter { diff --git a/DemoApp/Sources/Components/Snapshot/LocalParticipantSnapshotViewModel.swift b/DemoApp/Sources/Components/Snapshot/LocalParticipantSnapshotViewModel.swift index b9661545e..6ebe5167f 100644 --- a/DemoApp/Sources/Components/Snapshot/LocalParticipantSnapshotViewModel.swift +++ b/DemoApp/Sources/Components/Snapshot/LocalParticipantSnapshotViewModel.swift @@ -173,10 +173,15 @@ final class LocalParticipantSnapshotViewModel: NSObject, AVCapturePhotoCaptureDe } /// Provides the default value of the `LocalParticipantSnapshotViewModel` class. +#if swift(>=6.0) +struct LocalParticipantSnapshotViewModelKey: @preconcurrency InjectionKey { + @MainActor static var currentValue: LocalParticipantSnapshotViewModel = .init() +} +#else struct LocalParticipantSnapshotViewModelKey: InjectionKey { - @MainActor - static var currentValue: LocalParticipantSnapshotViewModel = .init() + @MainActor static var currentValue: LocalParticipantSnapshotViewModel = .init() } +#endif extension InjectedValues { /// Provides access to the `LocalParticipantSnapshotViewModel` class to the views and view models. diff --git a/DemoApp/Sources/Components/Snapshot/SnapshotTrigger.swift b/DemoApp/Sources/Components/Snapshot/SnapshotTrigger.swift index e8e7299ad..023b67b9e 100644 --- a/DemoApp/Sources/Components/Snapshot/SnapshotTrigger.swift +++ b/DemoApp/Sources/Components/Snapshot/SnapshotTrigger.swift @@ -30,10 +30,17 @@ final class StreamSnapshotTrigger: SnapshotTriggering, @unchecked Sendable { } /// Provides the default value of the `StreamSnapshotTrigger` class. +#if swift(>=6.0) +struct StreamSnapshotTriggerKey: @preconcurrency InjectionKey { + @MainActor + static var currentValue: StreamSnapshotTrigger = .init() +} +#else struct StreamSnapshotTriggerKey: InjectionKey { @MainActor static var currentValue: StreamSnapshotTrigger = .init() } +#endif extension InjectedValues { /// Provides access to the `StreamSnapshotTrigger` class to the views and view models. diff --git a/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift b/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift index 01acc9d86..e027dd600 100644 --- a/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift +++ b/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift @@ -6,7 +6,7 @@ import Foundation import PushKit /// Handles push notifications for CallKit integration. -@preconcurrency open class CallKitPushNotificationAdapter: NSObject, PKPushRegistryDelegate, ObservableObject { +@preconcurrency open class CallKitPushNotificationAdapter: NSObject, ObservableObject { /// Represents the keys that the Payload dictionary public enum PayloadKey: String { @@ -167,3 +167,9 @@ extension InjectedValues { set { Self[CallKitPushNotificationAdapter.self] = newValue } } } + +#if swift(>=6.0) +extension CallKitPushNotificationAdapter: @preconcurrency PKPushRegistryDelegate {} +#else +extension CallKitPushNotificationAdapter: PKPushRegistryDelegate {} +#endif diff --git a/Sources/StreamVideo/WebRTC/AudioSession.swift b/Sources/StreamVideo/WebRTC/AudioSession.swift index 1af74559b..dad93ccd9 100644 --- a/Sources/StreamVideo/WebRTC/AudioSession.swift +++ b/Sources/StreamVideo/WebRTC/AudioSession.swift @@ -7,7 +7,7 @@ import Foundation actor AudioSession { - private let rtcAudioSession: RTCAudioSession = RTCAudioSession.sharedInstance() + private var rtcAudioSession: RTCAudioSession = RTCAudioSession.sharedInstance() func configure( _ configuration: RTCAudioSessionConfiguration = .default, @@ -45,9 +45,9 @@ actor AudioSession { } nonisolated private func cleanup() { - rtcAudioSession.lockForConfiguration() - rtcAudioSession.isAudioEnabled = false - rtcAudioSession.unlockForConfiguration() + RTCAudioSession.sharedInstance().lockForConfiguration() + RTCAudioSession.sharedInstance().isAudioEnabled = false + RTCAudioSession.sharedInstance().unlockForConfiguration() } } diff --git a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift index 2b0e84ded..b254c5a05 100644 --- a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift +++ b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift @@ -8,7 +8,7 @@ import SwiftUI /// A property wrapper type that instantiates an observable object. @preconcurrency @propertyWrapper @available(iOS, introduced: 13, obsoleted: 14) @MainActor -public struct BackportStateObject: DynamicProperty +public struct BackportStateObject where ObjectType: Sendable, ObjectType.ObjectWillChangePublisher == ObservableObjectPublisher { /// Wrapper that helps with initialising without actually having an ObservableObject yet @@ -148,3 +148,9 @@ public struct PublishedObject: @unchecked Sendable { mutating get { _projectedValue.eraseToAnyPublisher() } } } + +#if swift(>=6.0) +extension BackportStateObject: @preconcurrency DynamicProperty {} +#else +extension BackportStateObject: DynamicProperty {} +#endif diff --git a/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift b/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift index 51139388a..d13964d20 100644 --- a/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift +++ b/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift @@ -12,9 +12,15 @@ public class Formatters { // MARK: - Formatters + Injection /// Provides the default value of the `Formatters` class. +#if swift(>=6.0) +enum FormattersKey: @preconcurrency InjectionKey { + @MainActor static var currentValue: Formatters = .init() +} +#else enum FormattersKey: InjectionKey { @MainActor static var currentValue: Formatters = .init() } +#endif extension InjectedValues { /// Provides access to the `Formatters` class to the views and view models. diff --git a/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift b/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift index aa1b4ab76..36ecd8202 100644 --- a/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift +++ b/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift @@ -8,13 +8,13 @@ import UIKit /// Publisher to read keyboard changes. public protocol KeyboardReadable { - var keyboardWillChangePublisher: AnyPublisher { get } - var keyboardDidChangePublisher: AnyPublisher { get } + @MainActor var keyboardWillChangePublisher: AnyPublisher { get } + @MainActor var keyboardDidChangePublisher: AnyPublisher { get } } /// Default implementation. extension KeyboardReadable { - @MainActor public var keyboardWillChangePublisher: AnyPublisher { + public var keyboardWillChangePublisher: AnyPublisher { Publishers.Merge( NotificationCenter.default .publisher(for: UIResponder.keyboardWillShowNotification) @@ -27,7 +27,7 @@ extension KeyboardReadable { .eraseToAnyPublisher() } - @MainActor public var keyboardDidChangePublisher: AnyPublisher { + public var keyboardDidChangePublisher: AnyPublisher { Publishers.Merge( NotificationCenter.default .publisher(for: UIResponder.keyboardDidShowNotification) diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift index 2c07d92bb..8dc552e04 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift @@ -27,7 +27,7 @@ protocol StreamAVPictureInPictureViewControlling { @available(iOS 15.0, *) final class StreamAVPictureInPictureVideoCallViewController: - AVPictureInPictureVideoCallViewController, StreamAVPictureInPictureViewControlling { + AVPictureInPictureVideoCallViewController { private let contentView: StreamPictureInPictureVideoRenderer = .init() @@ -71,3 +71,11 @@ final class StreamAVPictureInPictureVideoCallViewController: contentView.bounds = view.bounds } } + +#if swift(>=6.0) +@available(iOS 15.0, *) +extension StreamAVPictureInPictureVideoCallViewController: @preconcurrency StreamAVPictureInPictureViewControlling {} +#else +@available(iOS 15.0, *) +extension StreamAVPictureInPictureVideoCallViewController: StreamAVPictureInPictureViewControlling {} +#endif diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift index 6bc5e29cd..3c341a270 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift @@ -94,9 +94,15 @@ final class StreamPictureInPictureAdapter { } /// Provides the default value of the `StreamPictureInPictureAdapter` class. +#if swift(>=6.0) +enum StreamPictureInPictureAdapterKey: @preconcurrency InjectionKey { + @MainActor static var currentValue: StreamPictureInPictureAdapter = .init() +} +#else enum StreamPictureInPictureAdapterKey: InjectionKey { @MainActor static var currentValue: StreamPictureInPictureAdapter = .init() } +#endif extension InjectedValues { /// Provides access to the `StreamPictureInPictureAdapter` class to the views and view models. diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift index e952b3fac..a8b902298 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift @@ -8,7 +8,7 @@ import StreamVideo import StreamWebRTC /// A view that can be used to render an instance of `RTCVideoTrack` -final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { +final class StreamPictureInPictureVideoRenderer: UIView { /// The rendering track. var track: RTCVideoTrack? { @@ -264,3 +264,9 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { startFrameStreaming(for: track, on: window) } } + +#if swift(>=6.0) +extension StreamPictureInPictureVideoRenderer: @preconcurrency RTCVideoRenderer {} +#else +extension StreamPictureInPictureVideoRenderer: RTCVideoRenderer {} +#endif diff --git a/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift b/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift index 4cce743f8..4905090e9 100644 --- a/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift +++ b/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift @@ -47,9 +47,15 @@ final class VideoRendererPool { } } +#if swift(>=6.0) +extension VideoRendererPool: @preconcurrency InjectionKey { + @MainActor static var currentValue: VideoRendererPool = .init() +} +#else extension VideoRendererPool: InjectionKey { @MainActor static var currentValue: VideoRendererPool = .init() } +#endif extension InjectedValues { var videoRendererPool: VideoRendererPool { From 5863a9978bb234ae8df6623a8dc2a8f70a491098 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Thu, 4 Jul 2024 10:14:37 +0200 Subject: [PATCH 03/21] Fixed some crashes --- DemoApp/Sources/AppDelegate.swift | 13 +++++++------ .../WebRTC/StreamVideoCaptureHandler.swift | 12 +++++------- .../CallingViews/PreJoiningViewModel.swift | 8 ++++++-- Sources/StreamVideoSwiftUI/Utils/Camera.swift | 2 +- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/DemoApp/Sources/AppDelegate.swift b/DemoApp/Sources/AppDelegate.swift index f67d13ef6..530539687 100644 --- a/DemoApp/Sources/AppDelegate.swift +++ b/DemoApp/Sources/AppDelegate.swift @@ -95,15 +95,16 @@ class AppDelegate: NSObject, UIApplicationDelegate { // MARK: - Private Helpers private func setUpRemoteNotifications() { - UNUserNotificationCenter - .current() - .requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in + Task { @MainActor in + do { + let granted = try await UNUserNotificationCenter.current().requestAuthorization() if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } + UIApplication.shared.registerForRemoteNotifications() } + } catch { + log.error("Error requesting authorization: \(error.localizedDescription)") } + } } private func setUpPerformanceTracking() { diff --git a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift index 127faea83..9f67ce547 100644 --- a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift +++ b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift @@ -125,13 +125,11 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { } deinit { - executeOnMain { - NotificationCenter.default.removeObserver( - self, - name: UIDevice.orientationDidChangeNotification, - object: nil - ) - } + NotificationCenter.default.removeObserver( + self, + name: UIDevice.orientationDidChangeNotification, + object: nil + ) } } diff --git a/Sources/StreamVideoSwiftUI/CallingViews/PreJoiningViewModel.swift b/Sources/StreamVideoSwiftUI/CallingViews/PreJoiningViewModel.swift index 14687f2e8..d87418688 100644 --- a/Sources/StreamVideoSwiftUI/CallingViews/PreJoiningViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallingViews/PreJoiningViewModel.swift @@ -39,8 +39,12 @@ public class LobbyViewModel: ObservableObject, @unchecked Sendable { @available(iOS 14, *) func handleCameraPreviews() async { - let imageStream = (camera as? Camera)?.previewStream.dropFirst() - .map(\.image) + let previewStream = (camera as? Camera)?.previewStream + await handleStreamUpdates(previewStream) + } + + nonisolated private func handleStreamUpdates(_ previewStream: AsyncStream?) async { + let imageStream = previewStream?.dropFirst().map(\.image) guard let imageStream = imageStream else { return } diff --git a/Sources/StreamVideoSwiftUI/Utils/Camera.swift b/Sources/StreamVideoSwiftUI/Utils/Camera.swift index 11a44ee3a..60364ca81 100644 --- a/Sources/StreamVideoSwiftUI/Utils/Camera.swift +++ b/Sources/StreamVideoSwiftUI/Utils/Camera.swift @@ -93,7 +93,7 @@ class Camera: NSObject, @unchecked Sendable { var isPreviewPaused = false - lazy var previewStream: AsyncStream = { + nonisolated lazy var previewStream: AsyncStream = { AsyncStream { continuation in addToPreviewStream = { [weak self] ciImage in guard let self else { return } From 8367da62546146c65328a2c7ab43083c825bbb5e Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Thu, 4 Jul 2024 12:57:40 +0200 Subject: [PATCH 04/21] Fixed tests --- .../DemoMoreControls/DemoReactionButton.swift | 2 +- .../CallView/ParticipantsGridLayout.swift | 2 +- .../ScreenSharing/ScreenSharingView.swift | 2 +- .../StreamDeviceOrientationAdapter.swift | 48 +++++++++---------- .../ParticipantsGridLayout_Tests.swift | 2 + .../CallView/VideoRenderer_Tests.swift | 10 ++-- .../Utils/ToastView_Tests.swift | 10 ++-- .../Mock/CallController_Mock.swift | 3 +- .../BackgroundTaskScheduler_Tests.swift | 14 +++--- .../EventNotificationCenter_Tests.swift | 4 +- 10 files changed, 50 insertions(+), 47 deletions(-) diff --git a/DemoApp/Sources/Controls/DemoMoreControls/DemoReactionButton.swift b/DemoApp/Sources/Controls/DemoMoreControls/DemoReactionButton.swift index b9c48d519..5f22d6e4c 100644 --- a/DemoApp/Sources/Controls/DemoMoreControls/DemoReactionButton.swift +++ b/DemoApp/Sources/Controls/DemoMoreControls/DemoReactionButton.swift @@ -19,7 +19,7 @@ struct DemoReactionSelectorView: View { var body: some View { HStack { - if orientationAdapter.orientation?.isLandscape == true { + if orientationAdapter.orientation.isLandscape { HStack {} .frame(maxWidth: .infinity) contentView diff --git a/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift b/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift index 6ff5be9b5..ddd4a3c68 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift @@ -32,7 +32,7 @@ public struct ParticipantsGridLayout: View { public var body: some View { ZStack { - if orientationAdapter.orientation?.isPortrait == true { + if orientationAdapter.orientation.isPortrait { VideoParticipantsViewPortrait( viewFactory: viewFactory, call: call, diff --git a/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift b/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift index 5f7d6fd0b..2f84335dc 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift @@ -54,7 +54,7 @@ public struct ScreenSharingView: View { public var body: some View { VStack(spacing: innerItemSpace) { - if !viewModel.hideUIElements, orientationAdapter.orientation?.isPortrait == true || UIDevice.current.isIpad { + if !viewModel.hideUIElements, orientationAdapter.orientation.isPortrait || UIDevice.current.isIpad { Text("\(screenSharing.participant.name) presenting") .foregroundColor(colors.text) .padding() diff --git a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift index dded5a946..e6762a5e8 100644 --- a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift +++ b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift @@ -25,7 +25,7 @@ public class StreamDeviceOrientationAdapter: ObservableObject, @unchecked Sendab public typealias Provider = @MainActor @Sendable() -> StreamDeviceOrientation /// The default provider for device orientation based on platform. - @MainActor public static let defaultProvider: Provider = { + public static let defaultProvider: Provider = { #if canImport(UIKit) switch UIDevice.current.orientation { case .unknown, .portrait, .portraitUpsideDown: @@ -42,53 +42,51 @@ public class StreamDeviceOrientationAdapter: ObservableObject, @unchecked Sendab #endif } - @MainActor private lazy var provider: Provider = StreamDeviceOrientationAdapter.defaultProvider + private var provider: Provider private var notificationCancellable: AnyCancellable? /// The current orientation observed by the adapter. - @Published public private(set) var orientation: StreamDeviceOrientation? + @Published public private(set) var orientation: StreamDeviceOrientation /// Initializes an adapter for observing device orientation changes. /// - Parameters: /// - notificationCenter: The notification center to observe orientation changes. /// - provider: A custom provider for determining device orientation. - public init( + @MainActor public init( notificationCenter: NotificationCenter = .default, - _ provider: Provider? = nil + _ provider: @escaping Provider = StreamDeviceOrientationAdapter.defaultProvider ) { - Task { @MainActor in - if let provider { - self.provider = provider + self.provider = provider + orientation = provider() + + #if canImport(UIKit) + // Subscribe to orientation change notifications on UIKit platforms. + notificationCancellable = notificationCenter + .publisher(for: UIDevice.orientationDidChangeNotification) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.orientation = provider() // Update orientation based on the provider. } - orientation = self.provider() - - #if canImport(UIKit) - // Subscribe to orientation change notifications on UIKit platforms. - notificationCancellable = notificationCenter - .publisher(for: UIDevice.orientationDidChangeNotification) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.orientation = self.provider() // Update orientation based on the provider. - } - #endif - } + #endif } /// Cleans up resources when the adapter is deallocated. deinit { - cleanup() - } - - private func cleanup() { notificationCancellable?.cancel() // Cancel notification subscription. } } /// Provides the default value of the `StreamPictureInPictureAdapter` class. +#if swift(>=6.0) +enum StreamDeviceOrientationAdapterKey: @preconcurrency InjectionKey { + static var currentValue: StreamDeviceOrientationAdapter = .init() +} +#else enum StreamDeviceOrientationAdapterKey: InjectionKey { nonisolated(unsafe) static var currentValue: StreamDeviceOrientationAdapter = .init() } +#endif extension InjectedValues { /// Provides access to the `StreamDeviceOrientationAdapter` class to the views and view models. diff --git a/StreamVideoSwiftUITests/CallView/ParticipantsGridLayout_Tests.swift b/StreamVideoSwiftUITests/CallView/ParticipantsGridLayout_Tests.swift index 7697245db..8ad4f3dca 100644 --- a/StreamVideoSwiftUITests/CallView/ParticipantsGridLayout_Tests.swift +++ b/StreamVideoSwiftUITests/CallView/ParticipantsGridLayout_Tests.swift @@ -27,6 +27,7 @@ final class ParticipantsGridLayout_Tests: StreamVideoUITestCase { cachedLocation: nil ) + @MainActor override func setUp() { super.setUp() let streamVideo = StreamVideo.mock(httpClient: httpClient, callController: callController) @@ -34,6 +35,7 @@ final class ParticipantsGridLayout_Tests: StreamVideoUITestCase { InjectedValues[\.orientationAdapter] = orientationAdapter } + @MainActor override func tearDown() { mockedOrientation = nil orientationAdapter = nil diff --git a/StreamVideoSwiftUITests/CallView/VideoRenderer_Tests.swift b/StreamVideoSwiftUITests/CallView/VideoRenderer_Tests.swift index 7cfc03df6..1fdb7cb0a 100644 --- a/StreamVideoSwiftUITests/CallView/VideoRenderer_Tests.swift +++ b/StreamVideoSwiftUITests/CallView/VideoRenderer_Tests.swift @@ -13,7 +13,7 @@ final class VideoRenderer_Tests: XCTestCase { private var mockThermalStateObserver: MockThermalStateObserver! = .init() private var subject: VideoRenderer! - override func setUp() { + @MainActor override func setUp() { super.setUp() InjectedValues[\.thermalStateObserver] = mockThermalStateObserver @@ -27,22 +27,22 @@ final class VideoRenderer_Tests: XCTestCase { // MARK: - preferredFramesPerSecond - func testFPSForNominalThermalState() { + @MainActor func testFPSForNominalThermalState() { mockThermalStateObserver.state = .nominal XCTAssertEqual(subject.preferredFramesPerSecond, UIScreen.main.maximumFramesPerSecond) } - func testFPSForFairThermalState() { + @MainActor func testFPSForFairThermalState() { mockThermalStateObserver.state = .fair XCTAssertEqual(subject.preferredFramesPerSecond, UIScreen.main.maximumFramesPerSecond) } - func testFPSForSeriousThermalState() { + @MainActor func testFPSForSeriousThermalState() { mockThermalStateObserver.state = .serious XCTAssertEqual(subject.preferredFramesPerSecond, Int(Double(UIScreen.main.maximumFramesPerSecond) * 0.5)) } - func testFPSForCriticalThermalState() { + @MainActor func testFPSForCriticalThermalState() { mockThermalStateObserver.state = .critical XCTAssertEqual(subject.preferredFramesPerSecond, Int(Double(UIScreen.main.maximumFramesPerSecond) * 0.4)) } diff --git a/StreamVideoSwiftUITests/Utils/ToastView_Tests.swift b/StreamVideoSwiftUITests/Utils/ToastView_Tests.swift index b70a22d41..a61b51ff7 100644 --- a/StreamVideoSwiftUITests/Utils/ToastView_Tests.swift +++ b/StreamVideoSwiftUITests/Utils/ToastView_Tests.swift @@ -10,7 +10,7 @@ import XCTest final class ToastView_Tests: StreamVideoUITestCase { - func test_toastView_errorSnapshot() { + @MainActor func test_toastView_errorSnapshot() { // Given let toast = Toast(style: .error, message: "An error occurred.") let view = EmptyView() @@ -25,7 +25,7 @@ final class ToastView_Tests: StreamVideoUITestCase { AssertSnapshot(view, variants: snapshotVariants) } - func test_toastView_successSnapshot() { + @MainActor func test_toastView_successSnapshot() { // Given let toast = Toast(style: .success, message: "Something good occurred.") let view = EmptyView() @@ -40,7 +40,7 @@ final class ToastView_Tests: StreamVideoUITestCase { AssertSnapshot(view, variants: snapshotVariants) } - func test_toastView_warningSnapshot() { + @MainActor func test_toastView_warningSnapshot() { // Given let toast = Toast(style: .warning, message: "A warning occurred.") let view = EmptyView() @@ -55,7 +55,7 @@ final class ToastView_Tests: StreamVideoUITestCase { AssertSnapshot(view, variants: snapshotVariants) } - func test_toastView_infoSnapshot() { + @MainActor func test_toastView_infoSnapshot() { // Given let toast = Toast(style: .info, message: "An info message.") let view = EmptyView() @@ -70,7 +70,7 @@ final class ToastView_Tests: StreamVideoUITestCase { AssertSnapshot(view, variants: snapshotVariants) } - func test_toastView_errorSnapshotBottom() { + @MainActor func test_toastView_errorSnapshotBottom() { // Given let toast = Toast(style: .error, message: "An error occurred.", placement: .bottom) let view = EmptyView() diff --git a/StreamVideoTests/Mock/CallController_Mock.swift b/StreamVideoTests/Mock/CallController_Mock.swift index 63dd76265..b6f2e13c3 100644 --- a/StreamVideoTests/Mock/CallController_Mock.swift +++ b/StreamVideoTests/Mock/CallController_Mock.swift @@ -46,8 +46,9 @@ class CallController_Mock: CallController { notify: Bool = false ) async throws -> JoinCallResponse { webRTCClient.onParticipantsUpdated = { [weak self] participants in + guard let self else { return } executeOnMain { - self?.call?.state.participantsMap = participants + self.call?.state.participantsMap = participants } } return mockResponseBuilder.makeJoinCallResponse(cid: "\(callType):\(callId)") diff --git a/StreamVideoTests/WebSocketClient/BackgroundTaskScheduler_Tests.swift b/StreamVideoTests/WebSocketClient/BackgroundTaskScheduler_Tests.swift index 22915b129..1c615751b 100644 --- a/StreamVideoTests/WebSocketClient/BackgroundTaskScheduler_Tests.swift +++ b/StreamVideoTests/WebSocketClient/BackgroundTaskScheduler_Tests.swift @@ -6,8 +6,8 @@ import XCTest #if os(iOS) -final class IOSBackgroundTaskScheduler_Tests: XCTestCase { - func test_notifications_foreground() { +final class IOSBackgroundTaskScheduler_Tests: StreamVideoTestCase { + func test_notifications_foreground() async throws { // Given let scheduler = IOSBackgroundTaskScheduler() var calledBackground = false @@ -18,14 +18,15 @@ final class IOSBackgroundTaskScheduler_Tests: XCTestCase { ) // When - NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + await NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + try await waitForCallEvent() // Then XCTAssertTrue(calledForeground) XCTAssertFalse(calledBackground) } - func test_notifications_background() { + func test_notifications_background() async throws { // Given let scheduler = IOSBackgroundTaskScheduler() var calledBackground = false @@ -36,8 +37,9 @@ final class IOSBackgroundTaskScheduler_Tests: XCTestCase { ) // When - NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - + await NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + try await waitForCallEvent() + // Then XCTAssertFalse(calledForeground) XCTAssertTrue(calledBackground) diff --git a/StreamVideoTests/WebSocketClient/EventNotificationCenter_Tests.swift b/StreamVideoTests/WebSocketClient/EventNotificationCenter_Tests.swift index 129d99d5f..c60fdc22a 100644 --- a/StreamVideoTests/WebSocketClient/EventNotificationCenter_Tests.swift +++ b/StreamVideoTests/WebSocketClient/EventNotificationCenter_Tests.swift @@ -97,7 +97,7 @@ final class EventNotificationCenter_Tests: XCTestCase { let events = [TestEvent(), TestEvent(), TestEvent(), TestEvent()] // Feed events that should be posted and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false center.process(events.map { .internalEvent($0) }, postNotifications: true) { completionCalled = true } @@ -120,7 +120,7 @@ final class EventNotificationCenter_Tests: XCTestCase { let events = [TestEvent(), TestEvent(), TestEvent(), TestEvent()] // Feed events that should not be posted and catch the completion - var completionCalled = false + nonisolated(unsafe) var completionCalled = false center.process(events.map { .internalEvent($0) }, postNotifications: false) { completionCalled = true } From fb4f3381985b089295b7a055ef0b984775682e44 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Thu, 4 Jul 2024 12:59:54 +0200 Subject: [PATCH 05/21] Small fix for Swift6 --- .../DeviceOrientation/StreamDeviceOrientationAdapter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift index e6762a5e8..8952f5c1b 100644 --- a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift +++ b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift @@ -80,7 +80,7 @@ public class StreamDeviceOrientationAdapter: ObservableObject, @unchecked Sendab /// Provides the default value of the `StreamPictureInPictureAdapter` class. #if swift(>=6.0) enum StreamDeviceOrientationAdapterKey: @preconcurrency InjectionKey { - static var currentValue: StreamDeviceOrientationAdapter = .init() + @MainActor static var currentValue: StreamDeviceOrientationAdapter = .init() } #else enum StreamDeviceOrientationAdapterKey: InjectionKey { From b8073841e8a6726171d641a3e8cfe4f6a0887909 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Thu, 4 Jul 2024 16:38:05 +0200 Subject: [PATCH 06/21] Fixed a crash --- .../StreamPictureInPictureVideoRenderer.swift | 69 ++++++++++--------- StreamVideo.xcodeproj/project.pbxproj | 30 ++++---- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift index a8b902298..ed1d1eb11 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift @@ -113,45 +113,49 @@ final class StreamPictureInPictureVideoRenderer: UIView { // MARK: - Rendering lifecycle /// This method is being called from WebRTC and asks the container to set its size to the track's size. - func setSize(_ size: CGSize) { - trackSize = size + nonisolated func setSize(_ size: CGSize) { + DispatchQueue.main.async { + self.trackSize = size + } } - func renderFrame(_ frame: RTCVideoFrame?) { - guard let frame = frame else { - return - } + nonisolated func renderFrame(_ frame: RTCVideoFrame?) { + DispatchQueue.main.async { + guard let frame else { + return + } - // Update the trackSize and re-calculate rendering properties if the size - // has changed. - trackSize = .init(width: Int(frame.width), height: Int(frame.height)) + // Update the trackSize and re-calculate rendering properties if the size + // has changed. + self.trackSize = .init(width: Int(frame.width), height: Int(frame.height)) - log.debug("→ Received frame with trackSize:\(trackSize)") + log.debug("→ Received frame with trackSize:\(self.trackSize)") - defer { - handleFrameSkippingIfRequired() - } + defer { + self.handleFrameSkippingIfRequired() + } - guard shouldRenderFrame else { - log.debug("→ Skipping frame.") - return - } + guard self.shouldRenderFrame else { + log.debug("→ Skipping frame.") + return + } - let pixelBuffer: RTCVideoFrameBuffer? = { - if let i420buffer = frame.buffer as? RTCI420Buffer { - return i420buffer + let pixelBuffer: RTCVideoFrameBuffer? = { + if let i420buffer = frame.buffer as? RTCI420Buffer { + return i420buffer + } else { + return frame.buffer + } + }() + + if + let pixelBuffer = pixelBuffer, + let sampleBuffer = self.bufferTransformer.transformAndResizeIfRequired(pixelBuffer, targetSize: self.contentSize) { + log.debug("➕ Buffer for trackId:\(self.track?.trackId ?? "n/a") added.") + self.bufferPublisher.send(sampleBuffer) } else { - return frame.buffer + log.warning("Failed to convert \(type(of: frame.buffer)) CMSampleBuffer.") } - }() - - if - let pixelBuffer = pixelBuffer, - let sampleBuffer = bufferTransformer.transformAndResizeIfRequired(pixelBuffer, targetSize: contentSize) { - log.debug("➕ Buffer for trackId:\(track?.trackId ?? "n/a") added.") - bufferPublisher.send(sampleBuffer) - } else { - log.warning("Failed to convert \(type(of: frame.buffer)) CMSampleBuffer.") } } @@ -265,8 +269,5 @@ final class StreamPictureInPictureVideoRenderer: UIView { } } -#if swift(>=6.0) -extension StreamPictureInPictureVideoRenderer: @preconcurrency RTCVideoRenderer {} -#else extension StreamPictureInPictureVideoRenderer: RTCVideoRenderer {} -#endif +extension RTCVideoFrame: @unchecked Sendable {} diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index baf2c9b51..2a1ffa7bc 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -6148,7 +6148,7 @@ SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -6206,7 +6206,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -6265,7 +6265,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -6338,7 +6338,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Test; @@ -6387,7 +6387,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Test; @@ -6756,7 +6756,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -6806,7 +6806,7 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.getstream.iOS.VideoDemoApp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -6927,7 +6927,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -6977,7 +6977,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AdHoc io.getstream.iOS.VideoDemoApp"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -7016,7 +7016,7 @@ SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7055,7 +7055,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7131,7 +7131,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7169,7 +7169,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7248,7 +7248,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7287,7 +7287,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; From bd7393b76123da589f1843c0afec833775c32820 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Tue, 9 Jul 2024 11:41:48 +0200 Subject: [PATCH 07/21] Replaced usage of dispatchqueue.main with main actor task --- DemoApp/Sources/Components/CodeScanner/CodeScanner.swift | 2 +- DemoApp/Sources/Views/CallsView/CallsViewModel.swift | 2 +- Sources/StreamVideo/Controllers/CallController.swift | 4 ++-- Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift | 2 +- Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift | 2 +- .../WebSockets/Client/BackgroundTaskScheduler.swift | 2 +- .../CallingViews/iOS13/BackportStateObject.swift | 4 ++-- .../StreamPictureInPictureVideoRenderer.swift | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift index 2c9f1e259..0b5d2582b 100644 --- a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift +++ b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift @@ -226,7 +226,7 @@ final class ScannerViewController: UIViewController, UINavigationControllerDeleg case .notDetermined: requestCameraAccess { self.setupCaptureDevice() - DispatchQueue.main.async { + Task { @MainActor in self.setupSession() } } diff --git a/DemoApp/Sources/Views/CallsView/CallsViewModel.swift b/DemoApp/Sources/Views/CallsView/CallsViewModel.swift index ad4c12a51..d355b7467 100644 --- a/DemoApp/Sources/Views/CallsView/CallsViewModel.swift +++ b/DemoApp/Sources/Views/CallsView/CallsViewModel.swift @@ -52,7 +52,7 @@ final class CallsViewModel: ObservableObject { func subscribeToCallsUpdates() { callsController.$calls.sink { calls in - DispatchQueue.main.async { + Task { @MainActor in self.calls = calls } } diff --git a/Sources/StreamVideo/Controllers/CallController.swift b/Sources/StreamVideo/Controllers/CallController.swift index 3cb25931a..80da0546e 100644 --- a/Sources/StreamVideo/Controllers/CallController.swift +++ b/Sources/StreamVideo/Controllers/CallController.swift @@ -466,7 +466,7 @@ class CallController: @unchecked Sendable { private func handleParticipantsUpdated() { webRTCClient?.onParticipantsUpdated = { [weak self] participants in guard let self else { return } - DispatchQueue.main.async { + Task { @MainActor in self.call?.state.participantsMap = participants } } @@ -475,7 +475,7 @@ class CallController: @unchecked Sendable { private func handleParticipantCountUpdated() { webRTCClient?.onParticipantCountUpdated = { [weak self] participantCount in guard let self else { return } - DispatchQueue.main.async { + Task { @MainActor in self.call?.state.participantCount = participantCount } } diff --git a/Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift b/Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift index 27e8f5b3c..835166d53 100644 --- a/Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift +++ b/Sources/StreamVideo/WebRTC/SimulatorScreenCapturer.swift @@ -83,7 +83,7 @@ final class SimulatorScreenCapturer: RTCVideoCapturer, @unchecked Sendable { timeStampNs: Int64(CMTimeGetSeconds(frameTime) * 1e9) ) - DispatchQueue.main.async { [weak self] in + Task { @MainActor [weak self] in guard let self else { return } self.delegate?.capturer(self, didCapture: rtcVideoFrame) } diff --git a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift index 9f67ce547..aca1ee4d6 100644 --- a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift +++ b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift @@ -74,7 +74,7 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { } @objc private func updateRotation() { - DispatchQueue.main.async { + Task { @MainActor in self.sceneOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .unknown } } diff --git a/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift b/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift index b785a43b9..f50181769 100644 --- a/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift +++ b/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift @@ -42,7 +42,7 @@ class IOSBackgroundTaskScheduler: BackgroundTaskScheduler, @unchecked Sendable { var isActive = false let group = DispatchGroup() group.enter() - DispatchQueue.main.async { + Task { @MainActor in isActive = app?.applicationState == .active group.leave() } diff --git a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift index b254c5a05..3b7ee99ce 100644 --- a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift +++ b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift @@ -63,7 +63,7 @@ public struct PublishedObject: @unchecked Sendable { let parent = futureSelf.parent futureSelf.cancellable = wrappedValue.objectWillChange.sink { [parent] in parent.objectWillChange?() - DispatchQueue.main.async { + Task { @MainActor in publisher.send(wrappedValue) } } @@ -81,7 +81,7 @@ public struct PublishedObject: @unchecked Sendable { let parent = futureSelf.parent futureSelf.cancellable = wrappedValue?.objectWillChange.sink { [parent] in parent.objectWillChange?() - DispatchQueue.main.async { + Task { @MainActor in publisher.send(wrappedValue) } } diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift index ed1d1eb11..14eea7e0a 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift @@ -114,13 +114,13 @@ final class StreamPictureInPictureVideoRenderer: UIView { /// This method is being called from WebRTC and asks the container to set its size to the track's size. nonisolated func setSize(_ size: CGSize) { - DispatchQueue.main.async { + Task { @MainActor in self.trackSize = size } } nonisolated func renderFrame(_ frame: RTCVideoFrame?) { - DispatchQueue.main.async { + Task { @MainActor in guard let frame else { return } From 98f5941552ab236ba2a1ba3fe37e68ba90432ec6 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Tue, 9 Jul 2024 12:29:57 +0200 Subject: [PATCH 08/21] Removed executeOnMain helper --- Sources/StreamVideo/Call.swift | 26 +++++++++---------- .../Controllers/CallController.swift | 8 +++--- .../Controllers/CallsController.swift | 6 ++--- Sources/StreamVideo/StreamVideo.swift | 2 +- Sources/StreamVideo/Utils/Utils.swift | 6 ----- Sources/StreamVideo/WebRTC/AudioSession.swift | 4 +-- .../WebRTC/StreamVideoCaptureHandler.swift | 2 +- .../Client/BackgroundTaskScheduler.swift | 10 +++---- .../Mock/CallController_Mock.swift | 2 +- 9 files changed, 30 insertions(+), 36 deletions(-) diff --git a/Sources/StreamVideo/Call.swift b/Sources/StreamVideo/Call.swift index 1e8ae60cd..18d9f3e28 100644 --- a/Sources/StreamVideo/Call.swift +++ b/Sources/StreamVideo/Call.swift @@ -84,7 +84,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { coordinatorClient: coordinatorClient, callController: callController ) - executeOnMain { [weak self] in + Task { @MainActor [weak self] in self?.state.update(from: response) } } @@ -607,7 +607,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { granted: [request.permission], revoked: [] ) - executeOnMain { [weak self] in + Task { @MainActor [weak self] in guard let self else { return } self.state.removePermissionRequest(request: request) } @@ -1105,7 +1105,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { // MARK: - Internal internal func update(reconnectionStatus: ReconnectionStatus) { - executeOnMain { [weak self] in + Task { @MainActor [weak self] in guard let self else { return } if reconnectionStatus != self.state.reconnectionStatus { self.state.reconnectionStatus = reconnectionStatus @@ -1114,7 +1114,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { } internal func update(recordingState: RecordingState) { - executeOnMain { [weak self] in + Task { @MainActor [weak self] in self?.state.recordingState = recordingState } } @@ -1126,7 +1126,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { guard videoEvent.forCall(cid: cId) else { return } - executeOnMain { [weak self] in + Task { @MainActor [weak self] in guard let self else { return } self.state.updateState(from: videoEvent) } @@ -1179,7 +1179,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { } private func subscribeToOwnCapabilitiesChanges() { - executeOnMain { [weak self] in + Task { @MainActor [weak self] in guard let self else { return } self .state @@ -1193,7 +1193,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { private func subscribeToLocalCallSettingsChanges() { speaker.$status.dropFirst().sink { [weak self] status in guard let self else { return } - executeOnMain { + Task { @MainActor in let newState = self.state.callSettings.withUpdatedSpeakerState(status.boolValue) self.state.update(callSettings: newState) } @@ -1202,7 +1202,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { speaker.$audioOutputStatus.dropFirst().sink { [weak self] status in guard let self else { return } - executeOnMain { + Task { @MainActor in let newState = self.state.callSettings.withUpdatedAudioOutputState(status.boolValue) self.state.update(callSettings: newState) } @@ -1211,7 +1211,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { camera.$status.dropFirst().sink { [weak self] status in guard let self else { return } - executeOnMain { + Task { @MainActor in let newState = self.state.callSettings.withUpdatedVideoState(status.boolValue) self.state.update(callSettings: newState) } @@ -1220,7 +1220,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { camera.$direction.dropFirst().sink { [weak self] position in guard let self else { return } - executeOnMain { + Task { @MainActor in let newState = self.state.callSettings.withUpdatedCameraPosition(position) self.state.update(callSettings: newState) } @@ -1229,7 +1229,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { microphone.$status.dropFirst().sink { [weak self] status in guard let self else { return } - executeOnMain { + Task { @MainActor in let newState = self.state.callSettings.withUpdatedAudioState(status.boolValue) self.state.update(callSettings: newState) } @@ -1238,7 +1238,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { } private func subscribeToNoiseCancellationSettingsChanges() { - executeOnMain { [weak self] in + Task { @MainActor [weak self] in guard let self else { return } self .state @@ -1251,7 +1251,7 @@ public class Call: @unchecked Sendable, WSEventsSubscriber { } private func subscribeToTranscriptionSettingsChanges() { - executeOnMain { [weak self] in + Task { @MainActor [weak self] in guard let self else { return } self .state diff --git a/Sources/StreamVideo/Controllers/CallController.swift b/Sources/StreamVideo/Controllers/CallController.swift index 80da0546e..f8a0b8e03 100644 --- a/Sources/StreamVideo/Controllers/CallController.swift +++ b/Sources/StreamVideo/Controllers/CallController.swift @@ -403,7 +403,7 @@ class CallController: @unchecked Sendable { migratingFrom: String? ) async throws { if let migratingFrom { - executeOnMain { [weak self] in + Task { @MainActor [weak self] in self?.call?.state.reconnectionStatus = .migrating } webRTCClient?.prepareForMigration( @@ -448,7 +448,7 @@ class CallController: @unchecked Sendable { migrating: migratingFrom != nil ) let sessionId = webRTCClient?.sessionID ?? "" - executeOnMain { [weak self] in + Task { @MainActor [weak self] in self?.call?.state.sessionId = sessionId self?.call?.update(recordingState: response.call.recording ? .recording : .noRecording) self?.call?.state.ownCapabilities = response.ownCapabilities @@ -485,7 +485,7 @@ class CallController: @unchecked Sendable { switch state { case let .disconnected(source): log.debug("Signal channel disconnected") - executeOnMain { [weak self] in + Task { @MainActor [weak self] in self?.handleSignalChannelDisconnect(source: source) } case .connected(healthCheckInfo: _): @@ -493,7 +493,7 @@ class CallController: @unchecked Sendable { if reconnectionDate != nil { reconnectionDate = nil } - executeOnMain { [weak self] in + Task { @MainActor [weak self] in guard let self else { return } let status = self.call?.state.reconnectionStatus if status != .migrating { diff --git a/Sources/StreamVideo/Controllers/CallsController.swift b/Sources/StreamVideo/Controllers/CallsController.swift index d51a9b1fd..ded3ee1a3 100644 --- a/Sources/StreamVideo/Controllers/CallsController.swift +++ b/Sources/StreamVideo/Controllers/CallsController.swift @@ -143,7 +143,7 @@ public class CallsController: ObservableObject, @unchecked Sendable { guard let callEvent = event.rawValue as? WSCallEvent else { return } for (index, call) in calls.enumerated() { if call.cId == callEvent.callCid { - executeOnMain { [weak self] in + Task { @MainActor [weak self] in call.state.updateState(from: event) self?.calls[index] = call } @@ -155,7 +155,7 @@ public class CallsController: ObservableObject, @unchecked Sendable { callType: callCreated.call.type, callId: callCreated.call.id ) - executeOnMain { [weak self] in + Task { @MainActor [weak self] in call.state.update(from: callCreated) self?.calls.insert(call, at: 0) } @@ -167,7 +167,7 @@ public class CallsController: ObservableObject, @unchecked Sendable { callType: callResponse.call.type, callId: callResponse.call.id ) - executeOnMain { + Task { @MainActor in call.state.update(from: callResponse) } return call diff --git a/Sources/StreamVideo/StreamVideo.swift b/Sources/StreamVideo/StreamVideo.swift index 27e1788b9..597e515a4 100644 --- a/Sources/StreamVideo/StreamVideo.swift +++ b/Sources/StreamVideo/StreamVideo.swift @@ -690,7 +690,7 @@ extension StreamVideo: WSEventsSubscriber { callType: ringEvent.call.type, callId: ringEvent.call.id ) - executeOnMain { [weak self, call] in + Task { @MainActor [weak self, call] in guard let self else { return } call.state.update(from: ringEvent) self.state.ringingCall = call diff --git a/Sources/StreamVideo/Utils/Utils.swift b/Sources/StreamVideo/Utils/Utils.swift index 540f89731..e20dd440e 100644 --- a/Sources/StreamVideo/Utils/Utils.swift +++ b/Sources/StreamVideo/Utils/Utils.swift @@ -35,12 +35,6 @@ struct EventHandler { var cancel: () -> Void } -func executeOnMain(_ task: @Sendable @escaping @MainActor() -> Void) { - Task { - await task() - } -} - func infoPlistValue(for key: String) -> String? { Bundle.main.infoDictionary?[key] as? String } diff --git a/Sources/StreamVideo/WebRTC/AudioSession.swift b/Sources/StreamVideo/WebRTC/AudioSession.swift index dad93ccd9..eeefd95a9 100644 --- a/Sources/StreamVideo/WebRTC/AudioSession.swift +++ b/Sources/StreamVideo/WebRTC/AudioSession.swift @@ -3,11 +3,11 @@ // import Foundation -@preconcurrency import StreamWebRTC +import StreamWebRTC actor AudioSession { - private var rtcAudioSession: RTCAudioSession = RTCAudioSession.sharedInstance() + private let rtcAudioSession: RTCAudioSession = RTCAudioSession.sharedInstance() func configure( _ configuration: RTCAudioSessionConfiguration = .default, diff --git a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift index aca1ee4d6..caac6fee2 100644 --- a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift +++ b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift @@ -29,7 +29,7 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { context = CIContext(options: [CIContextOption.useSoftwareRenderer: false]) colorSpace = CGColorSpaceCreateDeviceRGB() super.init() - executeOnMain { + Task { @MainActor in NotificationCenter.default.addObserver( self, selector: #selector(self.updateRotation), diff --git a/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift b/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift index f50181769..ff2e1111b 100644 --- a/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift +++ b/Sources/StreamVideo/WebSockets/Client/BackgroundTaskScheduler.swift @@ -60,9 +60,9 @@ class IOSBackgroundTaskScheduler: BackgroundTaskScheduler, @unchecked Sendable { func endTask() { if let activeTask = activeBackgroundTask { - executeOnMain { - self.app?.endBackgroundTask(activeTask) - self.activeBackgroundTask = nil + Task { @MainActor [weak self] in + self?.app?.endBackgroundTask(activeTask) + self?.activeBackgroundTask = nil } } } @@ -77,7 +77,7 @@ class IOSBackgroundTaskScheduler: BackgroundTaskScheduler, @unchecked Sendable { self.onEnteringForeground = onEnteringForeground self.onEnteringBackground = onEnteringBackground - executeOnMain { + Task { @MainActor in NotificationCenter.default.addObserver( self, selector: #selector(self.handleAppDidEnterBackground), @@ -98,7 +98,7 @@ class IOSBackgroundTaskScheduler: BackgroundTaskScheduler, @unchecked Sendable { onEnteringForeground = {} onEnteringBackground = {} - executeOnMain { + Task { @MainActor in NotificationCenter.default.removeObserver( self, name: UIApplication.didEnterBackgroundNotification, diff --git a/StreamVideoTests/Mock/CallController_Mock.swift b/StreamVideoTests/Mock/CallController_Mock.swift index b6f2e13c3..fcd074475 100644 --- a/StreamVideoTests/Mock/CallController_Mock.swift +++ b/StreamVideoTests/Mock/CallController_Mock.swift @@ -47,7 +47,7 @@ class CallController_Mock: CallController { ) async throws -> JoinCallResponse { webRTCClient.onParticipantsUpdated = { [weak self] participants in guard let self else { return } - executeOnMain { + Task { @MainActor in self.call?.state.participantsMap = participants } } From 11d48887e04c77c6a884fb9a3a94a5f3c01d2eca Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Tue, 9 Jul 2024 12:40:10 +0200 Subject: [PATCH 09/21] PR remarks --- Sources/StreamVideoSwiftUI/Utils/Camera.swift | 27 ++++++++----------- .../StreamPictureInPictureVideoRenderer.swift | 11 +++----- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/Sources/StreamVideoSwiftUI/Utils/Camera.swift b/Sources/StreamVideoSwiftUI/Utils/Camera.swift index 60364ca81..613136ee6 100644 --- a/Sources/StreamVideoSwiftUI/Utils/Camera.swift +++ b/Sources/StreamVideoSwiftUI/Utils/Camera.swift @@ -5,6 +5,7 @@ @preconcurrency import AVFoundation import CoreImage import os.log +import StreamVideo import UIKit @available(iOS 14.0, *) @@ -68,7 +69,7 @@ class Camera: NSObject, @unchecked Sendable { private var captureDevice: AVCaptureDevice? { didSet { guard let captureDevice = captureDevice else { return } - logger.debug("Using capture device: \(captureDevice.localizedName)") + log.debug("Using capture device: \(captureDevice.localizedName)") sessionQueue.async { self.updateSessionForCaptureDevice(captureDevice) } @@ -130,7 +131,7 @@ class Camera: NSObject, @unchecked Sendable { let captureDevice = captureDevice, let deviceInput = try? AVCaptureDeviceInput(device: captureDevice) else { - logger.error("Failed to obtain video input.") + log.error("Failed to obtain video input.") return } @@ -140,12 +141,12 @@ class Camera: NSObject, @unchecked Sendable { videoOutput.setSampleBufferDelegate(self, queue: frameProcessingQueue) guard captureSession.canAddInput(deviceInput) else { - logger.error("Unable to add device input to capture session.") + log.error("Unable to add device input to capture session.") return } guard captureSession.canAddOutput(videoOutput) else { - logger.error("Unable to add video output to capture session.") + log.error("Unable to add video output to capture session.") return } @@ -165,19 +166,19 @@ class Camera: NSObject, @unchecked Sendable { private func checkAuthorization() async -> Bool { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: - logger.debug("Camera access authorized.") + log.debug("Camera access authorized.") return true case .notDetermined: - logger.debug("Camera access not determined.") + log.debug("Camera access not determined.") sessionQueue.suspend() let status = await AVCaptureDevice.requestAccess(for: .video) sessionQueue.resume() return status case .denied: - logger.debug("Camera access denied.") + log.debug("Camera access denied.") return false case .restricted: - logger.debug("Camera library access restricted.") + log.debug("Camera library access restricted.") return false @unknown default: return false @@ -189,7 +190,7 @@ class Camera: NSObject, @unchecked Sendable { do { return try AVCaptureDeviceInput(device: validDevice) } catch { - logger.error("Error getting capture device input: \(error.localizedDescription)") + log.error("Error getting capture device input: \(error.localizedDescription)") return nil } } @@ -226,7 +227,7 @@ class Camera: NSObject, @unchecked Sendable { func start() async { let authorized = await checkAuthorization() guard authorized else { - logger.error("Camera access was not authorized.") + log.error("Camera access was not authorized.") return } @@ -319,9 +320,3 @@ private extension UIScreen { } } } - -@available(iOS 14.0, *) -private let logger = Logger(subsystem: "com.apple.swiftplaygroundscontent.capturingphotos", category: "Camera") - -@available(iOS 14.0, *) -extension Logger: @unchecked Sendable {} diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift index 14eea7e0a..047663ca3 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift @@ -5,10 +5,10 @@ import Combine import Foundation import StreamVideo -import StreamWebRTC +@preconcurrency import StreamWebRTC /// A view that can be used to render an instance of `RTCVideoTrack` -final class StreamPictureInPictureVideoRenderer: UIView { +final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { /// The rendering track. var track: RTCVideoTrack? { @@ -120,8 +120,8 @@ final class StreamPictureInPictureVideoRenderer: UIView { } nonisolated func renderFrame(_ frame: RTCVideoFrame?) { - Task { @MainActor in - guard let frame else { + Task { @MainActor [weak self] in + guard let self, let frame else { return } @@ -268,6 +268,3 @@ final class StreamPictureInPictureVideoRenderer: UIView { startFrameStreaming(for: track, on: window) } } - -extension StreamPictureInPictureVideoRenderer: RTCVideoRenderer {} -extension RTCVideoFrame: @unchecked Sendable {} From ad54f074fb627d01f3b33d831ba52ab5f0f30c90 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 10 Jul 2024 13:24:16 +0200 Subject: [PATCH 10/21] Fixed issues with the latest Xcode beta --- DemoApp/Sources/AppDelegate.swift | 32 ++++++++----------- .../CallingView/DemoCallingViewModifier.swift | 1 + .../Views/CallsView/CallsViewModel.swift | 1 + DemoApp/Sources/Views/CustomCallView.swift | 1 + .../CallKitPushNotificationAdapter.swift | 11 ++----- .../StreamVideo/CallKit/CallKitService.swift | 1 - .../StreamVideo/Models/CallParticipant.swift | 2 +- .../RejectionReasonProvider.swift | 4 +-- .../StateMachine/Publisher+NextValue.swift | 2 ++ .../WebRTC/DefaultRTCMediaConstraints.swift | 2 +- Sources/StreamVideo/WebRTC/WebRTCClient.swift | 3 +- .../LocalParticipantViewModifier.swift | 15 +++++++-- .../StreamVideoSwiftUI/CallViewModel.swift | 12 +++++-- .../CallingViews/PreJoiningView.swift | 6 +++- .../iOS13/PreJoiningView_iOS13.swift | 1 + Sources/StreamVideoSwiftUI/Utils/Camera.swift | 4 +-- .../Utils/KeyboardReadable.swift | 4 +-- .../StreamPictureInPictureVideoRenderer.swift | 7 ++-- StreamVideo.xcodeproj/project.pbxproj | 30 ++++++++--------- 19 files changed, 77 insertions(+), 62 deletions(-) diff --git a/DemoApp/Sources/AppDelegate.swift b/DemoApp/Sources/AppDelegate.swift index 530539687..fa8a71ee7 100644 --- a/DemoApp/Sources/AppDelegate.swift +++ b/DemoApp/Sources/AppDelegate.swift @@ -7,7 +7,7 @@ import StreamVideo import SwiftUI import UIKit -class AppDelegate: NSObject, UIApplicationDelegate { +class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, @unchecked Sendable { @Injected(\.streamVideo) var streamVideo @@ -58,7 +58,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { AppState.shared.pushToken = deviceToken } - func userNotificationCenter( + nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void @@ -73,9 +73,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { if components.count >= 2 { let callType = components[0] let callId = components[1] - let call = streamVideo.call(callType: callType, callId: callId) - AppState.shared.activeCall = call - Task { + Task { @MainActor in + let streamVideo = self.streamVideo + let call = streamVideo.call(callType: callType, callId: callId) + AppState.shared.activeCall = call do { try Task.checkCancellation() try await streamVideo.connect() @@ -94,17 +95,16 @@ class AppDelegate: NSObject, UIApplicationDelegate { // MARK: - Private Helpers - private func setUpRemoteNotifications() { - Task { @MainActor in - do { - let granted = try await UNUserNotificationCenter.current().requestAuthorization() + nonisolated private func setUpRemoteNotifications() { + UNUserNotificationCenter + .current() + .requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in if granted { - UIApplication.shared.registerForRemoteNotifications() + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } } - } catch { - log.error("Error requesting authorization: \(error.localizedDescription)") } - } } private func setUpPerformanceTracking() { @@ -119,9 +119,3 @@ class AppDelegate: NSObject, UIApplicationDelegate { } } } - -#if swift(>=6.0) -extension AppDelegate: @preconcurrency UNUserNotificationCenterDelegate {} -#else -extension AppDelegate: UNUserNotificationCenterDelegate {} -#endif diff --git a/DemoApp/Sources/Views/CallView/CallingView/DemoCallingViewModifier.swift b/DemoApp/Sources/Views/CallView/CallingView/DemoCallingViewModifier.swift index 395d139f7..d287edf49 100644 --- a/DemoApp/Sources/Views/CallView/CallingView/DemoCallingViewModifier.swift +++ b/DemoApp/Sources/Views/CallView/CallingView/DemoCallingViewModifier.swift @@ -86,6 +86,7 @@ struct DemoCallingViewModifier: ViewModifier { return } + let streamVideo = self.streamVideo Task { do { try await streamVideo.connect() diff --git a/DemoApp/Sources/Views/CallsView/CallsViewModel.swift b/DemoApp/Sources/Views/CallsView/CallsViewModel.swift index d355b7467..b45c7ba72 100644 --- a/DemoApp/Sources/Views/CallsView/CallsViewModel.swift +++ b/DemoApp/Sources/Views/CallsView/CallsViewModel.swift @@ -41,6 +41,7 @@ final class CallsViewModel: ObservableObject { } func loadCalls() { + let callsController = self.callsController Task { do { try await callsController.loadNextCalls() diff --git a/DemoApp/Sources/Views/CustomCallView.swift b/DemoApp/Sources/Views/CustomCallView.swift index 702950567..460501adb 100644 --- a/DemoApp/Sources/Views/CustomCallView.swift +++ b/DemoApp/Sources/Views/CustomCallView.swift @@ -49,6 +49,7 @@ struct CustomCallView: View { } private func updateMicrophoneChecker() async { + let microphoneChecker = self.microphoneChecker if !viewModel.callSettings.audioOn { await microphoneChecker.startListening() } else { diff --git a/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift b/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift index e027dd600..50bc7f07b 100644 --- a/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift +++ b/Sources/StreamVideo/CallKit/CallKitPushNotificationAdapter.swift @@ -6,7 +6,7 @@ import Foundation import PushKit /// Handles push notifications for CallKit integration. -@preconcurrency open class CallKitPushNotificationAdapter: NSObject, ObservableObject { +@preconcurrency open class CallKitPushNotificationAdapter: NSObject, ObservableObject, PKPushRegistryDelegate, @unchecked Sendable { /// Represents the keys that the Payload dictionary public enum PayloadKey: String { @@ -18,7 +18,7 @@ import PushKit } /// Represents the content of a VoIP push notification. - public struct Content { + public struct Content: Sendable { var cid: String var localizedCallerName: String var callerId: String @@ -86,7 +86,6 @@ import PushKit } /// Delegate method called when the device receives a VoIP push notification. - @MainActor open func pushRegistry( _ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, @@ -167,9 +166,3 @@ extension InjectedValues { set { Self[CallKitPushNotificationAdapter.self] = newValue } } } - -#if swift(>=6.0) -extension CallKitPushNotificationAdapter: @preconcurrency PKPushRegistryDelegate {} -#else -extension CallKitPushNotificationAdapter: PKPushRegistryDelegate {} -#endif diff --git a/Sources/StreamVideo/CallKit/CallKitService.swift b/Sources/StreamVideo/CallKit/CallKitService.swift index d423e5f0f..c30d8ab77 100644 --- a/Sources/StreamVideo/CallKit/CallKitService.swift +++ b/Sources/StreamVideo/CallKit/CallKitService.swift @@ -89,7 +89,6 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable { /// - localizedCallerName: The localized caller name. /// - callerId: The caller's identifier. /// - completion: A closure to be called upon completion. - @MainActor open func reportIncomingCall( _ cid: String, localizedCallerName: String, diff --git a/Sources/StreamVideo/Models/CallParticipant.swift b/Sources/StreamVideo/Models/CallParticipant.swift index 967596494..b1ff62f4c 100644 --- a/Sources/StreamVideo/Models/CallParticipant.swift +++ b/Sources/StreamVideo/Models/CallParticipant.swift @@ -3,7 +3,7 @@ // import Foundation -@preconcurrency import StreamWebRTC +import StreamWebRTC /// Represents a participant in the call. public struct CallParticipant: Identifiable, Sendable, Equatable { diff --git a/Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift b/Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift index 422837e9b..c91f0db1d 100644 --- a/Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift +++ b/Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift @@ -6,7 +6,7 @@ import Combine import Foundation /// A protocol that provides a method to determine the rejection reason for a call. -public protocol RejectionReasonProviding { +public protocol RejectionReasonProviding: Sendable { /// Determines the rejection reason for a call with the specified call ID. /// @@ -23,7 +23,7 @@ public protocol RejectionReasonProviding { } /// A provider that determines the rejection reason for a call based on its state. -final class StreamRejectionReasonProvider: RejectionReasonProviding { +final class StreamRejectionReasonProvider: RejectionReasonProviding, @unchecked Sendable { /// The stream video associated with this provider. private weak var streamVideo: StreamVideo? diff --git a/Sources/StreamVideo/Utils/StateMachine/Publisher+NextValue.swift b/Sources/StreamVideo/Utils/StateMachine/Publisher+NextValue.swift index b0514293b..df1b4164a 100644 --- a/Sources/StreamVideo/Utils/StateMachine/Publisher+NextValue.swift +++ b/Sources/StreamVideo/Utils/StateMachine/Publisher+NextValue.swift @@ -28,6 +28,8 @@ extension Publisher { } }, receiveValue: { value in + // TODO: check with Ilias. + nonisolated(unsafe) let value = value if !receivedValue { continuation.resume(returning: value) // Resume only if value hasn't been received receivedValue = true diff --git a/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift b/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift index 222f827d1..09e622c8b 100644 --- a/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift +++ b/Sources/StreamVideo/WebRTC/DefaultRTCMediaConstraints.swift @@ -3,7 +3,7 @@ // import Foundation -@preconcurrency import StreamWebRTC +import StreamWebRTC extension RTCMediaConstraints: @unchecked Sendable { diff --git a/Sources/StreamVideo/WebRTC/WebRTCClient.swift b/Sources/StreamVideo/WebRTC/WebRTCClient.swift index 5914ea1b1..8c72c0cb3 100644 --- a/Sources/StreamVideo/WebRTC/WebRTCClient.swift +++ b/Sources/StreamVideo/WebRTC/WebRTCClient.swift @@ -34,10 +34,11 @@ class WebRTCClient: NSObject, @unchecked Sendable { } @Published var callParticipants = [String: CallParticipant]() { didSet { + let delay = participantUpdatesDelay if !scheduledUpdate { scheduledUpdate = true Task { - try? await Task.sleep(nanoseconds: participantUpdatesDelay) + try? await Task.sleep(nanoseconds: delay) lastUpdate = Date().timeIntervalSince1970 continuation?.yield([true]) scheduledUpdate = false diff --git a/Sources/StreamVideoSwiftUI/CallView/LocalParticipantViewModifier.swift b/Sources/StreamVideoSwiftUI/CallView/LocalParticipantViewModifier.swift index 5ef677d48..7faa654d7 100644 --- a/Sources/StreamVideoSwiftUI/CallView/LocalParticipantViewModifier.swift +++ b/Sources/StreamVideoSwiftUI/CallView/LocalParticipantViewModifier.swift @@ -59,6 +59,7 @@ public struct LocalParticipantViewModifier: ViewModifier { } } .onChange(of: callSettings, perform: { newValue in + let microphoneChecker = self.microphoneChecker Task { newValue.audioOn ? await microphoneChecker.startListening() @@ -133,8 +134,18 @@ public struct LocalParticipantViewModifier_iOS13: ViewModifier { .padding(.bottom, 2) } .padding(.all, showAllInfo ? 16 : 8) - .onAppear { Task { await microphoneChecker.startListening() } } - .onDisappear { Task { await microphoneChecker.stopListening() } } + .onAppear { + let microphoneChecker = self.microphoneChecker + Task { + await microphoneChecker.startListening() + } + } + .onDisappear { + let microphoneChecker = self.microphoneChecker + Task { + await microphoneChecker.stopListening() + } + } ) .applyDecorationModifierIfRequired( VideoCallParticipantOptionsModifier(participant: localParticipant, call: call), diff --git a/Sources/StreamVideoSwiftUI/CallViewModel.swift b/Sources/StreamVideoSwiftUI/CallViewModel.swift index 825296f6e..f2b483bfb 100644 --- a/Sources/StreamVideoSwiftUI/CallViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallViewModel.swift @@ -552,7 +552,10 @@ open class CallViewModel: ObservableObject { callingState = .idle isMinimized = false localVideoPrimary = false - Task { await audioRecorder.stopRecording() } + let audioRecorder = self.audioRecorder + Task { + await audioRecorder.stopRecording() + } } private func enterCall( @@ -597,7 +600,10 @@ open class CallViewModel: ObservableObject { log.error("Error starting a call", error: error) self.error = error callingState = .idle - Task { await audioRecorder.stopRecording() } + let audioRecorder = self.audioRecorder + Task { + await audioRecorder.stopRecording() + } enteringCallTask = nil } } @@ -628,7 +634,7 @@ open class CallViewModel: ObservableObject { Task { @MainActor [weak self] in guard let self = self else { return } log.debug("Detected ringing timeout, hanging up...") - handleCallHangUp(ringTimeout: true) + self.handleCallHangUp(ringTimeout: true) } } ) diff --git a/Sources/StreamVideoSwiftUI/CallingViews/PreJoiningView.swift b/Sources/StreamVideoSwiftUI/CallingViews/PreJoiningView.swift index 57e1b539c..62a45189e 100644 --- a/Sources/StreamVideoSwiftUI/CallingViews/PreJoiningView.swift +++ b/Sources/StreamVideoSwiftUI/CallingViews/PreJoiningView.swift @@ -57,6 +57,7 @@ public struct LobbyView: View { onCloseLobby: onCloseLobby ) .onChange(of: callSettings) { newValue in + let microphoneChecker = self.microphoneChecker Task { newValue.audioOn ? await microphoneChecker.startListening(ignoreActiveCall: true) @@ -87,7 +88,10 @@ struct LobbyContentView: View { HStack { Spacer() Button { - Task { await microphoneChecker.stopListening() } + let microphoneChecker = self.microphoneChecker + Task { + await microphoneChecker.stopListening() + } onCloseLobby() } label: { Image(systemName: "xmark") diff --git a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/PreJoiningView_iOS13.swift b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/PreJoiningView_iOS13.swift index 35b451c5d..742175b04 100644 --- a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/PreJoiningView_iOS13.swift +++ b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/PreJoiningView_iOS13.swift @@ -59,6 +59,7 @@ struct LobbyView_iOS13: View { onCloseLobby: onCloseLobby ) .onReceive(callViewModel.$callSettings) { newValue in + let microphoneChecker = self.microphoneChecker Task { newValue.audioOn ? await microphoneChecker.startListening(ignoreActiveCall: true) diff --git a/Sources/StreamVideoSwiftUI/Utils/Camera.swift b/Sources/StreamVideoSwiftUI/Utils/Camera.swift index 613136ee6..fc2f882ea 100644 --- a/Sources/StreamVideoSwiftUI/Utils/Camera.swift +++ b/Sources/StreamVideoSwiftUI/Utils/Camera.swift @@ -288,9 +288,9 @@ extension Camera: AVCaptureVideoDataOutputSampleBufferDelegate { didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection ) { - guard let pixelBuffer = sampleBuffer.imageBuffer else { return } - Task { @MainActor in + guard let pixelBuffer = sampleBuffer.imageBuffer else { return } + if connection.isVideoOrientationSupported, let videoOrientation = videoOrientationFor(deviceOrientation), diff --git a/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift b/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift index 36ecd8202..3b4e6353c 100644 --- a/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift +++ b/Sources/StreamVideoSwiftUI/Utils/KeyboardReadable.swift @@ -14,7 +14,7 @@ public protocol KeyboardReadable { /// Default implementation. extension KeyboardReadable { - public var keyboardWillChangePublisher: AnyPublisher { + @MainActor public var keyboardWillChangePublisher: AnyPublisher { Publishers.Merge( NotificationCenter.default .publisher(for: UIResponder.keyboardWillShowNotification) @@ -27,7 +27,7 @@ extension KeyboardReadable { .eraseToAnyPublisher() } - public var keyboardDidChangePublisher: AnyPublisher { + @MainActor public var keyboardDidChangePublisher: AnyPublisher { Publishers.Merge( NotificationCenter.default .publisher(for: UIResponder.keyboardDidShowNotification) diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift index 047663ca3..ba3be93c2 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift @@ -5,7 +5,7 @@ import Combine import Foundation import StreamVideo -@preconcurrency import StreamWebRTC +import StreamWebRTC /// A view that can be used to render an instance of `RTCVideoTrack` final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { @@ -120,11 +120,12 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { } nonisolated func renderFrame(_ frame: RTCVideoFrame?) { + nonisolated(unsafe) var unsafeFrame = frame Task { @MainActor [weak self] in - guard let self, let frame else { + guard let self, let frame = unsafeFrame else { return } - + // Update the trackSize and re-calculate rendering properties if the size // has changed. self.trackSize = .init(width: Int(frame.width), height: Int(frame.height)) diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 9c92535ba..332f072d2 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -6156,7 +6156,7 @@ SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -6214,7 +6214,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -6273,7 +6273,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -6346,7 +6346,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Test; @@ -6395,7 +6395,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Test; @@ -6764,7 +6764,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -6814,7 +6814,7 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.getstream.iOS.VideoDemoApp"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -6935,7 +6935,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -6985,7 +6985,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AdHoc io.getstream.iOS.VideoDemoApp"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -7024,7 +7024,7 @@ SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7063,7 +7063,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7139,7 +7139,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7177,7 +7177,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7256,7 +7256,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -7295,7 +7295,7 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; From 6934f2090d6c90bdf63bbddd08a2a50c8695312d Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 10 Jul 2024 13:40:55 +0200 Subject: [PATCH 11/21] Fixed LLC warnings --- Sources/StreamVideo/Models/CoordinatorModels.swift | 1 + .../WebRTC/Statistics/Statistics+Convenience.swift | 2 ++ .../WebRTC/StreamVideoCaptureHandler.swift | 14 +++++++++----- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Sources/StreamVideo/Models/CoordinatorModels.swift b/Sources/StreamVideo/Models/CoordinatorModels.swift index 75e046b38..89eced602 100644 --- a/Sources/StreamVideo/Models/CoordinatorModels.swift +++ b/Sources/StreamVideo/Models/CoordinatorModels.swift @@ -29,6 +29,7 @@ extension UnpinResponse: @unchecked Sendable {} extension GoLiveResponse: @unchecked Sendable {} extension SendReactionResponse: @unchecked Sendable {} extension CallStateResponseFields: @unchecked Sendable {} +extension KeyPath: @unchecked Sendable {} public struct FetchingLocationError: Error {} diff --git a/Sources/StreamVideo/WebRTC/Statistics/Statistics+Convenience.swift b/Sources/StreamVideo/WebRTC/Statistics/Statistics+Convenience.swift index 608173082..6c6a29bf4 100644 --- a/Sources/StreamVideo/WebRTC/Statistics/Statistics+Convenience.swift +++ b/Sources/StreamVideo/WebRTC/Statistics/Statistics+Convenience.swift @@ -121,3 +121,5 @@ extension StreamStatisticsReportProtocol { } } } + +extension StreamRTCStatistics.CodingKeys: @unchecked Sendable {} diff --git a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift index caac6fee2..da3a4f555 100644 --- a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift +++ b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift @@ -15,6 +15,7 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { var sceneOrientation: UIInterfaceOrientation = .unknown var currentCameraPosition: AVCaptureDevice.Position = .front private let handleRotation: Bool + private var notification: NSNotification.Name? private lazy var serialActor = SerialActor() @@ -30,6 +31,7 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { colorSpace = CGColorSpaceCreateDeviceRGB() super.init() Task { @MainActor in + notification = UIDevice.orientationDidChangeNotification NotificationCenter.default.addObserver( self, selector: #selector(self.updateRotation), @@ -125,11 +127,13 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { } deinit { - NotificationCenter.default.removeObserver( - self, - name: UIDevice.orientationDidChangeNotification, - object: nil - ) + if let notification { + NotificationCenter.default.removeObserver( + self, + name: notification, + object: nil + ) + } } } From 84c548ac8b2af74f220315b06b70a6c4cc95f8b6 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 10 Jul 2024 14:56:12 +0200 Subject: [PATCH 12/21] Fixed some warnings --- .../Components/CodeScanner/CodeScanner.swift | 92 ++++++++++--------- .../StreamDeviceOrientationAdapter.swift | 2 +- .../StreamPictureInPictureVideoRenderer.swift | 4 +- 3 files changed, 51 insertions(+), 47 deletions(-) diff --git a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift index 0b5d2582b..5b4f322b8 100644 --- a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift +++ b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // -import AVFoundation +@preconcurrency import AVFoundation import SwiftUI import UIKit @@ -412,7 +412,7 @@ final class ScannerViewController: UIViewController, UINavigationControllerDeleg lastTime = Date() } - func metadataOutput( + nonisolated func metadataOutput( _ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection @@ -421,45 +421,47 @@ final class ScannerViewController: UIViewController, UINavigationControllerDeleg guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } guard let stringValue = readableObject.stringValue else { return } - guard didFinishScanning == false else { return } + Task { @MainActor in + guard didFinishScanning == false else { return } - let photoSettings = AVCapturePhotoSettings() - guard !isCapturing else { return } - isCapturing = true + let photoSettings = AVCapturePhotoSettings() + guard !isCapturing else { return } + isCapturing = true - handler = { [self] image in - let result = ScanResult( - string: stringValue, - type: readableObject.type, - image: image, - corners: readableObject.corners - ) + handler = { [self] image in + let result = ScanResult( + string: stringValue, + type: readableObject.type, + image: image, + corners: readableObject.corners + ) - switch parentView.scanMode { - case .once: - found(result) - // make sure we only trigger scan once per use - didFinishScanning = true - - case .manual: - if !didFinishScanning, isWithinManualCaptureInterval() { + switch parentView.scanMode { + case .once: found(result) + // make sure we only trigger scan once per use didFinishScanning = true - } - case .oncePerCode: - if !codesFound.contains(stringValue) { - codesFound.insert(stringValue) - found(result) - } - - case .continuous: - if isPastScanInterval() { - found(result) + case .manual: + if !didFinishScanning, isWithinManualCaptureInterval() { + found(result) + didFinishScanning = true + } + + case .oncePerCode: + if !codesFound.contains(stringValue) { + codesFound.insert(stringValue) + found(result) + } + + case .continuous: + if isPastScanInterval() { + found(result) + } } } + photoOutput.capturePhoto(with: photoSettings, delegate: self) } - photoOutput.capturePhoto(with: photoSettings, delegate: self) } } @@ -495,31 +497,33 @@ extension ScannerViewController: AVCapturePhotoCaptureDelegate, AVCaptureMetadat extension ScannerViewController { - func photoOutput( + nonisolated func photoOutput( _ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error? ) { - isCapturing = false - guard let imageData = photo.fileDataRepresentation() else { - print("Error while generating image from photo capture data.") - return - } - guard let qrImage = UIImage(data: imageData) else { - print("Unable to generate UIImage from image data.") - return + Task { @MainActor in + isCapturing = false + guard let imageData = photo.fileDataRepresentation() else { + print("Error while generating image from photo capture data.") + return + } + guard let qrImage = UIImage(data: imageData) else { + print("Unable to generate UIImage from image data.") + return + } + handler?(qrImage) } - handler?(qrImage) } - func photoOutput( + nonisolated func photoOutput( _ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings ) { AudioServicesDisposeSystemSoundID(1108) } - func photoOutput( + nonisolated func photoOutput( _ output: AVCapturePhotoOutput, didCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings ) { diff --git a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift index 8952f5c1b..9fed28e90 100644 --- a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift +++ b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift @@ -84,7 +84,7 @@ enum StreamDeviceOrientationAdapterKey: @preconcurrency InjectionKey { } #else enum StreamDeviceOrientationAdapterKey: InjectionKey { - nonisolated(unsafe) static var currentValue: StreamDeviceOrientationAdapter = .init() + static var currentValue: StreamDeviceOrientationAdapter = .init() } #endif diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift index ba3be93c2..e97fb8e65 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift @@ -5,7 +5,7 @@ import Combine import Foundation import StreamVideo -import StreamWebRTC +@preconcurrency import StreamWebRTC /// A view that can be used to render an instance of `RTCVideoTrack` final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { @@ -120,7 +120,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { } nonisolated func renderFrame(_ frame: RTCVideoFrame?) { - nonisolated(unsafe) var unsafeFrame = frame + nonisolated(unsafe) let unsafeFrame = frame Task { @MainActor [weak self] in guard let self, let frame = unsafeFrame else { return From edf9fb9812d92d1b23cd3e500d7a5d63aa61313d Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Wed, 10 Jul 2024 17:46:53 +0200 Subject: [PATCH 13/21] Updated Xcode version for CI --- .github/workflows/smoke-checks.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml index 71cb8b5c8..e3258006e 100644 --- a/.github/workflows/smoke-checks.yml +++ b/.github/workflows/smoke-checks.yml @@ -51,8 +51,8 @@ jobs: run: bundle exec fastlane test device:"${{ env.IOS_SIMULATOR_DEVICE }}" timeout-minutes: 40 env: - XCODE_VERSION: "15.2" # the most stable pair of Xcode - IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.2)" # and iOS + XCODE_VERSION: "15.4" # the most stable pair of Xcode + IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" # and iOS - name: Get branch name id: get_branch_name run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT @@ -108,8 +108,8 @@ jobs: run: bundle exec fastlane test_swiftui device:"${{ env.IOS_SIMULATOR_DEVICE }}" record:${{ github.event.inputs.swiftui_snapshots }} timeout-minutes: 40 env: - XCODE_VERSION: "15.2" # the most stable pair of Xcode - IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.2)" # and iOS + XCODE_VERSION: "15.4" # the most stable pair of Xcode + IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" # and iOS - name: Parse xcresult if: failure() run: | @@ -141,8 +141,8 @@ jobs: run: bundle exec fastlane test_uikit device:"${{ env.IOS_SIMULATOR_DEVICE }}" record:${{ github.event.inputs.uikit_snapshots }} timeout-minutes: 40 env: - XCODE_VERSION: "15.2" # the most stable pair of Xcode - IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.2)" # and iOS + XCODE_VERSION: "15.4" # the most stable pair of Xcode + IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" # and iOS - name: Parse xcresult if: failure() run: | @@ -309,8 +309,8 @@ jobs: run: bundle exec fastlane test_e2e device:"${{ env.IOS_SIMULATOR_DEVICE }}" batch:'${{ matrix.batch }}' test_without_building:true timeout-minutes: 60 env: - XCODE_VERSION: "15.2" # the most stable pair of Xcode - IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.2)" # and iOS + XCODE_VERSION: "15.4" # the most stable pair of Xcode + IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" # and iOS MATRIX_SIZE: ${{ strategy.job-total }} STREAM_SDK_TEST_APP: ${{ secrets.STREAM_SDK_TEST_APP }} STREAM_SDK_TEST_ACCOUNT_EMAIL: ${{ secrets.STREAM_SDK_TEST_ACCOUNT_EMAIL }} From e1939416ee04b8f23f560371126e2b4250f2bb2f Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Thu, 11 Jul 2024 16:55:33 +0200 Subject: [PATCH 14/21] Fixed some warnings --- .../Components/Chat/DemoChatViewFactory.swift | 3 +- StreamVideoTests/Call/Call_Tests.swift | 33 +++++++++---------- .../CallKit/CallKitServiceTests.swift | 4 +-- .../CallState/CallState_Tests.swift | 29 ++++++++-------- .../Controllers/CallController_Tests.swift | 29 ++++++++-------- .../Controllers/CallsController_Tests.swift | 11 +++---- .../IntegrationTests/IntegrationTest.swift | 4 +-- StreamVideoTests/Mock/EventBatcher_Mock.swift | 6 ++-- .../Mock/EventNotificationCenter_Mock.swift | 2 +- StreamVideoTests/Mock/StreamVideo_Mock.swift | 6 ++-- .../Mock/WebSocketEngine_Mock.swift | 2 +- StreamVideoTests/StreamVideoTestCase.swift | 2 +- StreamVideoTests/StreamVideo_Tests.swift | 10 +++--- .../TestUtils/AssertTestQueue.swift | 2 +- .../TestUtils/VirtualTime/VirtualTimer.swift | 2 +- .../LastParticipantAutoLeavePolicyTests.swift | 4 +-- ...StateMachineStageAcceptedStage_Tests.swift | 2 +- ...tateMachineStageAcceptingStage_Tests.swift | 6 ++-- ...allStateMachineStageErrorStage_Tests.swift | 4 +-- ...CallStateMachineStageIdleStage_Tests.swift | 2 +- ...llStateMachineStageJoinedStage_Tests.swift | 2 +- ...lStateMachineStageJoiningStage_Tests.swift | 6 ++-- ...StateMachineStageRejectedStage_Tests.swift | 2 +- ...tateMachineStageRejectingStage_Tests.swift | 6 ++-- .../StreamCallStateMachine_Tests.swift | 1 - .../Utils/StreamCallAudioRecorderTests.swift | 23 ++++++------- .../WebRTC/WebRTCClient_Tests.swift | 4 +-- .../EventNotificationCenter_Tests.swift | 2 +- 28 files changed, 101 insertions(+), 108 deletions(-) diff --git a/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift b/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift index 26b8e99a7..490691725 100644 --- a/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift +++ b/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift @@ -8,14 +8,13 @@ import StreamChatSwiftUI import class StreamVideoSwiftUI.CallViewModel import SwiftUI -@MainActor final class DemoChatViewFactory { @Injected(\.chatClient) var chatClient: ChatClient private init() {} - static let shared = DemoChatViewFactory() + @MainActor static let shared = DemoChatViewFactory() func makeReactionsOverlayView( channel: ChatChannel, diff --git a/StreamVideoTests/Call/Call_Tests.swift b/StreamVideoTests/Call/Call_Tests.swift index 35a31d244..2dfe994ad 100644 --- a/StreamVideoTests/Call/Call_Tests.swift +++ b/StreamVideoTests/Call/Call_Tests.swift @@ -5,7 +5,6 @@ @testable import StreamVideo import XCTest -@MainActor final class Call_Tests: StreamVideoTestCase { let callType = "default" @@ -14,7 +13,7 @@ final class Call_Tests: StreamVideoTestCase { let userId = "test" let mockResponseBuilder = MockResponseBuilder() - func test_updateState_fromCallAcceptedEvent() { + @MainActor func test_updateState_fromCallAcceptedEvent() { // Given let call = streamVideo?.call(callType: callType, callId: callId) let callResponse = mockResponseBuilder.makeCallResponse( @@ -41,7 +40,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call?.state.session != nil) } - func test_updateState_fromCallRejectedEvent() { + @MainActor func test_updateState_fromCallRejectedEvent() { // Given let call = streamVideo?.call(callType: callType, callId: callId) let callResponse = mockResponseBuilder.makeCallResponse( @@ -68,7 +67,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call?.state.session != nil) } - func test_updateState_fromCallUpdatedEvent() { + @MainActor func test_updateState_fromCallUpdatedEvent() { // Given let call = streamVideo?.call(callType: callType, callId: callId) let callResponse = mockResponseBuilder.makeCallResponse( @@ -92,7 +91,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call?.state.session != nil) } - func test_updateState_fromRecordingStartedEvent() { + @MainActor func test_updateState_fromRecordingStartedEvent() { // Given let call = streamVideo?.call(callType: callType, callId: callId) let event = CallRecordingStartedEvent(callCid: callCid, createdAt: Date()) @@ -104,7 +103,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call?.state.recordingState == .recording) } - func test_updateState_fromRecordingStoppedEvent() { + @MainActor func test_updateState_fromRecordingStoppedEvent() { // Given let call = streamVideo?.call(callType: callType, callId: callId) let event = CallRecordingStoppedEvent(callCid: callCid, createdAt: Date()) @@ -116,7 +115,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call?.state.recordingState == .noRecording) } - func test_updateState_fromPermissionsEvent() { + @MainActor func test_updateState_fromPermissionsEvent() { // Given let videoConfig = VideoConfig.dummy() let userResponse = mockResponseBuilder.makeUserResponse(id: "testuser") @@ -155,7 +154,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call.currentUserHasCapability(.sendVideo) == false) } - func test_updateState_fromMemberAddedEvent() { + @MainActor func test_updateState_fromMemberAddedEvent() { // Given let call = streamVideo?.call(callType: callType, callId: callId) let callResponse = mockResponseBuilder.makeCallResponse( @@ -177,7 +176,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call?.state.members.first?.id == userId) } - func test_updateState_fromMemberRemovedEvent() { + @MainActor func test_updateState_fromMemberRemovedEvent() { // Given let userId = "test" let call = streamVideo?.call(callType: callType, callId: callId) @@ -199,7 +198,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call?.state.members.isEmpty == true) } - func test_updateState_fromMemberUpdatedEvent() { + @MainActor func test_updateState_fromMemberUpdatedEvent() { // Given let userId = "test" let call = streamVideo?.call(callType: callType, callId: callId) @@ -223,7 +222,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssert(call?.state.members.first?.user.name == "newname") } - func test_updateState_fromTranscriptionStoppedEvent() throws { + @MainActor func test_updateState_fromTranscriptionStoppedEvent() throws { try assertUpdateState( with: [ .init( @@ -237,7 +236,7 @@ final class Call_Tests: StreamVideoTestCase { ) } - func test_updateState_fromTranscriptionStartedEvent() throws { + @MainActor func test_updateState_fromTranscriptionStartedEvent() throws { try assertUpdateState( with: [ .init( @@ -251,7 +250,7 @@ final class Call_Tests: StreamVideoTestCase { ) } - func test_updateState_transcriptionStarted_fromTranscriptionFailedEvent() throws { + @MainActor func test_updateState_transcriptionStarted_fromTranscriptionFailedEvent() throws { try assertUpdateState( with: [ .init( @@ -272,7 +271,7 @@ final class Call_Tests: StreamVideoTestCase { ) } - func test_call_duration() async throws { + @MainActor func test_call_duration() async throws { // Given let call = streamVideo?.call(callType: callType, callId: callId) let startDate = Date() @@ -305,7 +304,7 @@ final class Call_Tests: StreamVideoTestCase { // MARK: - join - func test_join_callControllerWasCalledOnlyOnce() async throws { + @MainActor func test_join_callControllerWasCalledOnlyOnce() async throws { LogConfig.level = .debug let mockCallController = MockCallController() let call = MockCall(.dummy(callController: mockCallController)) @@ -331,7 +330,7 @@ final class Call_Tests: StreamVideoTestCase { XCTAssertEqual(mockCallController.timesJoinWasCalled, 1) } - func test_call_customSorting() async throws { + @MainActor func test_call_customSorting() async throws { // Given let nameComparator: StreamSortComparator = { comparison($0, $1, keyPath: \.name) @@ -354,7 +353,7 @@ final class Call_Tests: StreamVideoTestCase { // MARK: - Private helpers - private func assertUpdateState( + @MainActor private func assertUpdateState( with steps: [UpdateStateStep], file: StaticString = #file, line: UInt = #line diff --git a/StreamVideoTests/CallKit/CallKitServiceTests.swift b/StreamVideoTests/CallKit/CallKitServiceTests.swift index 432628f04..9066c2e27 100644 --- a/StreamVideoTests/CallKit/CallKitServiceTests.swift +++ b/StreamVideoTests/CallKit/CallKitServiceTests.swift @@ -495,7 +495,7 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { } } - guard case let .reportCall(uuid, dateEnded, reason) = callProvider.invocations.last else { + guard case let .reportCall(_, _, reason) = callProvider.invocations.last else { XCTFail(file: file, line: line) return } @@ -629,7 +629,7 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable { subject.streamVideo = mockedStreamVideo } - private func waitExpectation( + @MainActor private func waitExpectation( timeout: TimeInterval = defaultTimeout, description: String = "Wait expectation" ) async { diff --git a/StreamVideoTests/CallState/CallState_Tests.swift b/StreamVideoTests/CallState/CallState_Tests.swift index a43a0bd62..32a215ddc 100644 --- a/StreamVideoTests/CallState/CallState_Tests.swift +++ b/StreamVideoTests/CallState/CallState_Tests.swift @@ -6,11 +6,10 @@ import Foundation @testable import StreamVideo import XCTest -@MainActor final class CallState_Tests: XCTestCase { /// Test the `didUpdate(_:)` function by combining existing and newly added participants. - func test_didUpdate_combinesExistingAndNewParticipants() { + @MainActor func test_didUpdate_combinesExistingAndNewParticipants() { assertParticipantsUpdate( initial: [ CallParticipant.dummy(id: "1") @@ -21,7 +20,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function with sorting participants using defaultComparators. - func test_didUpdate_sortsParticipantsUsingDefaultComparators() { + @MainActor func test_didUpdate_sortsParticipantsUsingDefaultComparators() { assertParticipantsUpdate( initial: [ CallParticipant.dummy(id: "1", name: "Zane") @@ -32,7 +31,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function by ensuring that duplicated participants are not added. - func test_didUpdate_avoidsDuplicateParticipants() { + @MainActor func test_didUpdate_avoidsDuplicateParticipants() { assertParticipantsUpdate( initial: [ CallParticipant.dummy(id: "1") @@ -43,7 +42,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function by sorting participants using the `isSpeaking` property. - func test_didUpdate_sortsParticipantsBySpeaking() { + @MainActor func test_didUpdate_sortsParticipantsBySpeaking() { assertParticipantsUpdate( initial: [ .dummy(id: "1", isSpeaking: false), @@ -62,7 +61,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function by sorting participants using the `hasVideo` property. - func test_didUpdate_sortsParticipantsByVideo() { + @MainActor func test_didUpdate_sortsParticipantsByVideo() { assertParticipantsUpdate( initial: [ .dummy(id: "1", hasVideo: false), @@ -81,7 +80,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function by sorting participants using the `hasAudio` property. - func test_didUpdate_sortsParticipantsByAudio() { + @MainActor func test_didUpdate_sortsParticipantsByAudio() { assertParticipantsUpdate( initial: [ .dummy(id: "1", hasAudio: false), @@ -100,7 +99,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function by sorting participants using the `userId` property. - func test_didUpdate_sortsParticipantsByUserId() { + @MainActor func test_didUpdate_sortsParticipantsByUserId() { assertParticipantsUpdate( initial: [ .dummy(id: "1", userId: "B"), @@ -119,7 +118,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function by sorting participants based on speaking and video properties. - func test_didUpdate_sortsParticipantsBySpeakingAndVideo() { + @MainActor func test_didUpdate_sortsParticipantsBySpeakingAndVideo() { assertParticipantsUpdate( initial: [ .dummy(id: "1", hasVideo: false, isSpeaking: true), @@ -138,7 +137,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function by sorting participants based on joined time and audio properties. - func test_didUpdate_sortsParticipantsByUserIdAndAudio() { + @MainActor func test_didUpdate_sortsParticipantsByUserIdAndAudio() { assertParticipantsUpdate( initial: [ .dummy(id: "1", hasAudio: true), @@ -157,7 +156,7 @@ final class CallState_Tests: XCTestCase { } /// Test the `didUpdate(_:)` function by sorting participants based on userId and speaking properties. - func test_didUpdate_sortsParticipantsByUserIdAndSpeaking() { + @MainActor func test_didUpdate_sortsParticipantsByUserIdAndSpeaking() { assertParticipantsUpdate( initial: [ .dummy(id: "1", userId: "A", showTrack: false, isSpeaking: false), @@ -176,7 +175,7 @@ final class CallState_Tests: XCTestCase { } /// Test the execution time of `didUpdate` with many merge/add/remove operations. - func test_didUpdate_performanceWithManyParticipants_timeExecutionIsLessThanMaxDuration() { + @MainActor func test_didUpdate_performanceWithManyParticipants_timeExecutionIsLessThanMaxDuration() { let subject = CallState() let cycleCount = 250 @@ -200,7 +199,7 @@ final class CallState_Tests: XCTestCase { // MARK: - Private helpers - private func assertParticipantsUpdate( + @MainActor private func assertParticipantsUpdate( initial: [CallParticipant], update: @escaping (_ initial: [CallParticipant]) -> [CallParticipant], expectedTransformer: @escaping ([CallParticipant]) -> [CallParticipant], @@ -237,7 +236,7 @@ final class CallState_Tests: XCTestCase { } } - private func add(count: Int, namePrefix: Int = 0, in subject: CallState) { + @MainActor private func add(count: Int, namePrefix: Int = 0, in subject: CallState) { let existingParticipants = subject.participants let newParticipants = makeCallParticipants(count: count, nameSuffix: namePrefix) @@ -245,7 +244,7 @@ final class CallState_Tests: XCTestCase { .reduce(into: [String: CallParticipant]()) { $0[$1.id] = $1 } } - private func dropFirst(count: Int, from subject: CallState) { + @MainActor private func dropFirst(count: Int, from subject: CallState) { subject.participantsMap = subject .participants .dropFirst(count) diff --git a/StreamVideoTests/Controllers/CallController_Tests.swift b/StreamVideoTests/Controllers/CallController_Tests.swift index 002c03357..1c7861fdf 100644 --- a/StreamVideoTests/Controllers/CallController_Tests.swift +++ b/StreamVideoTests/Controllers/CallController_Tests.swift @@ -6,7 +6,6 @@ import SwiftProtobuf import XCTest -@MainActor final class CallController_Tests: ControllerTestCase { private var webRTCClient: WebRTCClient! @@ -24,7 +23,7 @@ final class CallController_Tests: ControllerTestCase { ) } - func test_callController_joinCall_webRTCClientSignalChannelUsesTheExpectedConnectURL() async throws { + @MainActor func test_callController_joinCall_webRTCClientSignalChannelUsesTheExpectedConnectURL() async throws { // Given webRTCClient = makeWebRTCClient() let callController = makeCallController() @@ -41,7 +40,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssertEqual(webRTCClient.signalChannel?.connectURL.absoluteString, "wss://test.com/ws") } - func test_callController_reconnectionSuccess() async throws { + @MainActor func test_callController_reconnectionSuccess() async throws { // Given webRTCClient = makeWebRTCClient() let callController = makeCallController(shouldReconnect: true) @@ -87,7 +86,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(callController.call?.state.reconnectionStatus == .connected) } - func test_callController_migrationSuccess() async throws { + @MainActor func test_callController_migrationSuccess() async throws { // Given webRTCClient = makeWebRTCClient() let callController = makeCallController(shouldReconnect: true) @@ -132,7 +131,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(callController.call?.state.reconnectionStatus == .connected) } - func test_callController_reconnectionFailure() async throws { + @MainActor func test_callController_reconnectionFailure() async throws { // Given webRTCClient = makeWebRTCClient() let callController = makeCallController() @@ -167,7 +166,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(callController.call == nil) } - func test_callController_updateCallInfo() async throws { + @MainActor func test_callController_updateCallInfo() async throws { // Given webRTCClient = makeWebRTCClient() let callController = makeCallController() @@ -189,8 +188,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(callController.call?.state.backstage == true) } - @MainActor - func test_callController_updateRecordingState() async throws { + @MainActor func test_callController_updateRecordingState() async throws { // Given webRTCClient = makeWebRTCClient() let callController = makeCallController(recording: true) @@ -211,8 +209,7 @@ final class CallController_Tests: ControllerTestCase { try await XCTAssertWithDelay(callController.call?.state.recordingState == .recording) } - @MainActor - func test_callController_updateRecordingStateDifferentCallCid() async throws { + @MainActor func test_callController_updateRecordingStateDifferentCallCid() async throws { // Given webRTCClient = makeWebRTCClient() let callController = makeCallController() @@ -233,7 +230,7 @@ final class CallController_Tests: ControllerTestCase { try await XCTAssertWithDelay(callController.call?.state.recordingState == .noRecording) } - func test_callController_cleanup() async throws { + @MainActor func test_callController_cleanup() async throws { // Given webRTCClient = makeWebRTCClient() let callController = makeCallController() @@ -253,7 +250,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(callController.call == nil) } - func test_callController_changeAudioState() async throws { + @MainActor func test_callController_changeAudioState() async throws { // Given webRTCClient = try makeWebRTCClientWithMuteStatesResponse() let callController = makeCallController() @@ -273,7 +270,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(webRTCClient.callSettings.audioOn == false) } - func test_callController_changeVideoState() async throws { + @MainActor func test_callController_changeVideoState() async throws { // Given webRTCClient = try makeWebRTCClientWithMuteStatesResponse() let callController = makeCallController() @@ -293,7 +290,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(webRTCClient.callSettings.videoOn == false) } - func test_callController_changeTrackVisibility() async throws { + @MainActor func test_callController_changeTrackVisibility() async throws { // Given let sessionId = "test" webRTCClient = makeWebRTCClient() @@ -317,7 +314,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(updated?.showTrack == true) } - func test_callController_updateTrackSize() async throws { + @MainActor func test_callController_updateTrackSize() async throws { // Given let sessionId = "test" let size = CGSize(width: 100, height: 100) @@ -342,7 +339,7 @@ final class CallController_Tests: ControllerTestCase { XCTAssert(updated?.trackSize == size) } - func test_callController_pinAndUnpin() async throws { + @MainActor func test_callController_pinAndUnpin() async throws { // Given let sessionId = "test" webRTCClient = makeWebRTCClient() diff --git a/StreamVideoTests/Controllers/CallsController_Tests.swift b/StreamVideoTests/Controllers/CallsController_Tests.swift index 22cda991e..11bb0cdda 100644 --- a/StreamVideoTests/Controllers/CallsController_Tests.swift +++ b/StreamVideoTests/Controllers/CallsController_Tests.swift @@ -5,12 +5,11 @@ @testable import StreamVideo import XCTest -@MainActor final class CallsController_Tests: ControllerTestCase { let mockResponseBuilder = MockResponseBuilder() - func test_callsController_queryCalls() async throws { + @MainActor func test_callsController_queryCalls() async throws { // Given let callsController = try makeTestCallsController() @@ -21,7 +20,7 @@ final class CallsController_Tests: ControllerTestCase { XCTAssert(callsController.calls.count == 2) } - func test_callsController_newCall() async throws { + @MainActor func test_callsController_newCall() async throws { // Given let callsController = try makeTestCallsController() @@ -42,7 +41,7 @@ final class CallsController_Tests: ControllerTestCase { try await XCTAssertWithDelay(callsController.calls.count == 3) } - func test_callsController_updatedCall() async throws { + @MainActor func test_callsController_updatedCall() async throws { // Given let callsController = try makeTestCallsController() @@ -65,7 +64,7 @@ final class CallsController_Tests: ControllerTestCase { try await XCTAssertWithDelay(callsController.calls[0].state.backstage == true) } - func test_callsController_rewatchCalls() async throws { + @MainActor func test_callsController_rewatchCalls() async throws { // Given let callsController = try makeTestCallsController() @@ -82,7 +81,7 @@ final class CallsController_Tests: ControllerTestCase { XCTAssertEqual(httpClient.requestCounter, 2) } - func test_callsController_noWatchingCalls() async throws { + @MainActor func test_callsController_noWatchingCalls() async throws { // Given let callsController = try makeTestCallsController(watch: false) diff --git a/StreamVideoTests/IntegrationTests/IntegrationTest.swift b/StreamVideoTests/IntegrationTests/IntegrationTest.swift index 100bb3b77..ab8fb7ba2 100644 --- a/StreamVideoTests/IntegrationTests/IntegrationTest.swift +++ b/StreamVideoTests/IntegrationTests/IntegrationTest.swift @@ -66,7 +66,7 @@ class IntegrationTest: XCTestCase { } // TODO: extract code between these two assertNext methods - func assertNext( + @MainActor func assertNext( _ s: AsyncStream, timeout seconds: TimeInterval = 1, _ assertion: @Sendable @escaping (Output) -> Bool @@ -86,7 +86,7 @@ class IntegrationTest: XCTestCase { await safeFulfillment(of: [expectation], timeout: seconds) } - func assertNext( + @MainActor func assertNext( _ p: some Publisher, timeout seconds: TimeInterval = 1, _ assertion: @escaping (Output) -> Bool, diff --git a/StreamVideoTests/Mock/EventBatcher_Mock.swift b/StreamVideoTests/Mock/EventBatcher_Mock.swift index 21b1ec3ee..3289b7edd 100644 --- a/StreamVideoTests/Mock/EventBatcher_Mock.swift +++ b/StreamVideoTests/Mock/EventBatcher_Mock.swift @@ -6,15 +6,15 @@ import Foundation @testable import StreamVideo import protocol StreamVideo.Timer -final class EventBatcher_Mock: EventBatcher { +final class EventBatcher_Mock: EventBatcher, @unchecked Sendable { var currentBatch: [WrappedEvent] = [] - let handler: (_ batch: [WrappedEvent], _ completion: @escaping () -> Void) -> Void + let handler: (_ batch: [WrappedEvent], _ completion: @Sendable @escaping () -> Void) -> Void init( period: TimeInterval = 0, timerType: Timer.Type = DefaultTimer.self, - handler: @escaping (_ batch: [WrappedEvent], _ completion: @escaping () -> Void) -> Void + handler: @escaping (_ batch: [WrappedEvent], _ completion: @Sendable @escaping () -> Void) -> Void ) { self.handler = handler } diff --git a/StreamVideoTests/Mock/EventNotificationCenter_Mock.swift b/StreamVideoTests/Mock/EventNotificationCenter_Mock.swift index 5e2cbbc33..609065c70 100644 --- a/StreamVideoTests/Mock/EventNotificationCenter_Mock.swift +++ b/StreamVideoTests/Mock/EventNotificationCenter_Mock.swift @@ -13,7 +13,7 @@ final class EventNotificationCenter_Mock: EventNotificationCenter { override func process( _ events: [WrappedEvent], postNotifications: Bool = true, - completion: (() -> Void)? = nil + completion: (@Sendable() -> Void)? = nil ) { super.process(events, postNotifications: postNotifications, completion: completion) diff --git a/StreamVideoTests/Mock/StreamVideo_Mock.swift b/StreamVideoTests/Mock/StreamVideo_Mock.swift index 862fb750a..2cd401804 100644 --- a/StreamVideoTests/Mock/StreamVideo_Mock.swift +++ b/StreamVideoTests/Mock/StreamVideo_Mock.swift @@ -5,15 +5,15 @@ @testable import StreamVideo extension StreamVideo { - static var apiKey = "key1" - static var mockUser = User( + static let apiKey = "key1" + static let mockUser = User( id: "testuser", name: "Test User", imageURL: ImageFactory.get(0), customData: [:] ) - static var mockToken = + static let mockToken = UserToken( rawValue: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdHJlYW0tdmlkZW8tZ29AdjAuMS4wIiwic3ViIjoidXNlci90ZXN0dXNlciIsImlhdCI6MTY2NjY5ODczMSwidXNlcl9pZCI6InRlc3R1c2VyIn0.h4lnaF6OFYaNPjeK8uFkKirR5kHtj1vAKuipq3A5nM0" ) diff --git a/StreamVideoTests/Mock/WebSocketEngine_Mock.swift b/StreamVideoTests/Mock/WebSocketEngine_Mock.swift index 1ff5fa0d1..acebb22de 100644 --- a/StreamVideoTests/Mock/WebSocketEngine_Mock.swift +++ b/StreamVideoTests/Mock/WebSocketEngine_Mock.swift @@ -5,7 +5,7 @@ import Foundation @testable import StreamVideo -final class WebSocketEngine_Mock: WebSocketEngine { +final class WebSocketEngine_Mock: WebSocketEngine, @unchecked Sendable { var request: URLRequest var sessionConfiguration: URLSessionConfiguration diff --git a/StreamVideoTests/StreamVideoTestCase.swift b/StreamVideoTests/StreamVideoTestCase.swift index 6edbfb631..4b551bfcc 100644 --- a/StreamVideoTests/StreamVideoTestCase.swift +++ b/StreamVideoTests/StreamVideoTestCase.swift @@ -5,7 +5,7 @@ @testable import StreamVideo import XCTest -open class StreamVideoTestCase: XCTestCase { +open class StreamVideoTestCase: XCTestCase, @unchecked Sendable { public var streamVideo: StreamVideo! var httpClient: HTTPClient_Mock! = HTTPClient_Mock() diff --git a/StreamVideoTests/StreamVideo_Tests.swift b/StreamVideoTests/StreamVideo_Tests.swift index 07b27374f..01869df4c 100644 --- a/StreamVideoTests/StreamVideo_Tests.swift +++ b/StreamVideoTests/StreamVideo_Tests.swift @@ -57,7 +57,7 @@ final class StreamVideo_Tests: StreamVideoTestCase { XCTAssert(call.callId == callId) } - func test_streamVideo_activeCallAndLeave() async throws { + @MainActor func test_streamVideo_activeCallAndLeave() async throws { // Given let streamVideo = StreamVideo.mock(httpClient: HTTPClient_Mock()) self.streamVideo = streamVideo @@ -80,7 +80,7 @@ final class StreamVideo_Tests: StreamVideoTestCase { XCTAssert(streamVideo.state.activeCall == nil) } - func test_streamVideo_ringCallAccept() async throws { + @MainActor func test_streamVideo_ringCallAccept() async throws { let httpClient = httpClientWithGetCallResponse() let streamVideo = StreamVideo.mock(httpClient: httpClient) self.streamVideo = streamVideo @@ -117,7 +117,7 @@ final class StreamVideo_Tests: StreamVideoTestCase { XCTAssert(streamVideo.state.activeCall?.cId == call.cId) } - func test_streamVideo_ringCallReject() async throws { + @MainActor func test_streamVideo_ringCallReject() async throws { let httpClient = httpClientWithGetCallResponse() let rejectCallResponse = RejectCallResponse(duration: "1") let data = try! JSONEncoder.default.encode(rejectCallResponse) @@ -145,7 +145,7 @@ final class StreamVideo_Tests: StreamVideoTestCase { } } - func test_streamVideo_incomingCallAccept() async throws { + @MainActor func test_streamVideo_incomingCallAccept() async throws { // Given let streamVideo = StreamVideo.mock(httpClient: HTTPClient_Mock()) self.streamVideo = streamVideo @@ -180,7 +180,7 @@ final class StreamVideo_Tests: StreamVideoTestCase { XCTAssert(streamVideo.state.activeCall?.cId == call.cId) } - func test_streamVideo_incomingCallReject() async throws { + @MainActor func test_streamVideo_incomingCallReject() async throws { // Given let httpClient = HTTPClient_Mock() let data = try! JSONEncoder().encode(RejectCallResponse(duration: "1")) diff --git a/StreamVideoTests/TestUtils/AssertTestQueue.swift b/StreamVideoTests/TestUtils/AssertTestQueue.swift index e55554594..c078d1717 100644 --- a/StreamVideoTests/TestUtils/AssertTestQueue.swift +++ b/StreamVideoTests/TestUtils/AssertTestQueue.swift @@ -16,7 +16,7 @@ func AssertTestQueue(withId id: UUID, file: StaticString = #filePath, line: UInt } extension DispatchQueue { - private static let queueIdKey = DispatchSpecificKey() + nonisolated(unsafe) private static let queueIdKey = DispatchSpecificKey() /// Creates a new queue which can be later identified by the id. static func testQueue(withId id: UUID) -> DispatchQueue { diff --git a/StreamVideoTests/TestUtils/VirtualTime/VirtualTimer.swift b/StreamVideoTests/TestUtils/VirtualTime/VirtualTimer.swift index 19199393e..d268cbe7a 100644 --- a/StreamVideoTests/TestUtils/VirtualTime/VirtualTimer.swift +++ b/StreamVideoTests/TestUtils/VirtualTime/VirtualTimer.swift @@ -7,7 +7,7 @@ import Foundation import protocol StreamVideo.Timer struct VirtualTimeTimer: Timer { - static var time: VirtualTime! + nonisolated(unsafe) static var time: VirtualTime! static func invalidate() { time.invalidate() diff --git a/StreamVideoTests/Utils/ParticipantAutoLeavePolicy/LastParticipantAutoLeavePolicyTests.swift b/StreamVideoTests/Utils/ParticipantAutoLeavePolicy/LastParticipantAutoLeavePolicyTests.swift index cd0634d5a..bb20d7d0d 100644 --- a/StreamVideoTests/Utils/ParticipantAutoLeavePolicy/LastParticipantAutoLeavePolicyTests.swift +++ b/StreamVideoTests/Utils/ParticipantAutoLeavePolicy/LastParticipantAutoLeavePolicyTests.swift @@ -4,7 +4,7 @@ import Combine @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class LastParticipantAutoLeavePolicyTests: XCTestCase, @unchecked Sendable { @@ -90,7 +90,7 @@ final class LastParticipantAutoLeavePolicyTests: XCTestCase, @unchecked Sendable // MARK: - Private helpers - private func assertPolicyWasTriggered( + @MainActor private func assertPolicyWasTriggered( _ expectsTrigger: Bool, ringingCall: Call?, activeCall: Call, diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageAcceptedStage_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageAcceptedStage_Tests.swift index 0f0de63b3..f75d10105 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageAcceptedStage_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageAcceptedStage_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class StreamCallStateMachineStageAcceptedStage_Tests: StreamVideoTestCase, @unchecked Sendable { diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageAcceptingStage_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageAcceptingStage_Tests.swift index 7400f076d..bf8faf0de 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageAcceptingStage_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageAcceptingStage_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class StreamCallStateMachineStageAcceptingStage_Tests: StreamVideoTestCase, @unchecked Sendable { @@ -37,7 +37,7 @@ final class StreamCallStateMachineStageAcceptingStage_Tests: StreamVideoTestCase // MARK: - Test Transition - func testTransition() async { + @MainActor func testTransition() async { for nextStage in allOtherStages { if validOtherStages.contains(nextStage.id) { subject.transition = { self.transitionedToStage = $0 } @@ -50,7 +50,7 @@ final class StreamCallStateMachineStageAcceptingStage_Tests: StreamVideoTestCase } } - func testTransitionAfterError() async { + @MainActor func testTransitionAfterError() async { for nextStage in allOtherStages { if validOtherStages.contains(nextStage.id) { subject = .accepting(call) { throw TestError() } diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageErrorStage_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageErrorStage_Tests.swift index 729e62c11..73fdbce6e 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageErrorStage_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageErrorStage_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class StreamCallStateMachineStageErrorStage_Tests: StreamVideoTestCase, @unchecked Sendable { @@ -43,7 +43,7 @@ final class StreamCallStateMachineStageErrorStage_Tests: StreamVideoTestCase, @u // MARK: - Test Transition - func testTransition() async { + @MainActor func testTransition() async { for nextStage in allOtherStages { if validOtherStages.contains(nextStage.id) { subject.transition = { self.transitionedToStage = $0 } diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageIdleStage_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageIdleStage_Tests.swift index c2c80488b..e4e347529 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageIdleStage_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageIdleStage_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class StreamCallStateMachineStageIdleStage_Tests: StreamVideoTestCase, @unchecked Sendable { diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageJoinedStage_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageJoinedStage_Tests.swift index c066d991d..e3fbf4694 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageJoinedStage_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageJoinedStage_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class StreamCallStateMachineStageJoinedStage_Tests: StreamVideoTestCase, @unchecked Sendable { diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageJoiningStage_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageJoiningStage_Tests.swift index 432ddd70b..0f46316c9 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageJoiningStage_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageJoiningStage_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class StreamCallStateMachineStageJoiningStage_Tests: StreamVideoTestCase, @unchecked Sendable { @@ -37,7 +37,7 @@ final class StreamCallStateMachineStageJoiningStage_Tests: StreamVideoTestCase, // MARK: - Test Transition - func testTransition() async { + @MainActor func testTransition() async { for nextStage in allOtherStages { if validOtherStages.contains(nextStage.id) { subject.transition = { self.transitionedToStage = $0 } @@ -50,7 +50,7 @@ final class StreamCallStateMachineStageJoiningStage_Tests: StreamVideoTestCase, } } - func testTransitionAfterError() async { + @MainActor func testTransitionAfterError() async { for nextStage in allOtherStages { if validOtherStages.contains(nextStage.id) { subject = .joining(call) { throw TestError() } diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageRejectedStage_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageRejectedStage_Tests.swift index 6248fd3d0..60b134c31 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageRejectedStage_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageRejectedStage_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class StreamCallStateMachineStageRejectedStage_Tests: StreamVideoTestCase, @unchecked Sendable { diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageRejectingStage_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageRejectingStage_Tests.swift index f8dbb1e50..f8bb76a4e 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageRejectingStage_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachineStageRejectingStage_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import XCTest +import XCTest final class StreamCallStateMachineStageRejectingStage_Tests: StreamVideoTestCase, @unchecked Sendable { @@ -38,7 +38,7 @@ final class StreamCallStateMachineStageRejectingStage_Tests: StreamVideoTestCase // MARK: - Test Transition - func testTransition() async { + @MainActor func testTransition() async { for nextStage in allOtherStages { if validOtherStages.contains(nextStage.id) { subject.transition = { self.transitionedToStage = $0 } @@ -51,7 +51,7 @@ final class StreamCallStateMachineStageRejectingStage_Tests: StreamVideoTestCase } } - func testTransitionAfterError() async { + @MainActor func testTransitionAfterError() async { for nextStage in allOtherStages { if validOtherStages.contains(nextStage.id) { subject = .rejecting(call) { throw TestError() } diff --git a/StreamVideoTests/Utils/StateMachine/CallStateMachine/StreamCallStateMachine_Tests.swift b/StreamVideoTests/Utils/StateMachine/CallStateMachine/StreamCallStateMachine_Tests.swift index d5e386911..8f3912139 100644 --- a/StreamVideoTests/Utils/StateMachine/CallStateMachine/StreamCallStateMachine_Tests.swift +++ b/StreamVideoTests/Utils/StateMachine/CallStateMachine/StreamCallStateMachine_Tests.swift @@ -57,7 +57,6 @@ final class StreamCallStateMachineTests: StreamVideoTestCase { // func testNextStageShouldBe() async throws { // Given - let expectation = XCTestExpectation(description: "Next stage retrieved") let response = AcceptCallResponse(duration: "123") // When diff --git a/StreamVideoTests/Utils/StreamCallAudioRecorderTests.swift b/StreamVideoTests/Utils/StreamCallAudioRecorderTests.swift index 82be7688e..db5f086c8 100644 --- a/StreamVideoTests/Utils/StreamCallAudioRecorderTests.swift +++ b/StreamVideoTests/Utils/StreamCallAudioRecorderTests.swift @@ -7,7 +7,7 @@ import Combine @testable import StreamVideo import XCTest -final class StreamAudioRecorderTests: XCTestCase { +final class StreamAudioRecorderTests: XCTestCase, @unchecked Sendable { private lazy var builder: AVAudioRecorderBuilder! = .init(cachedResult: mockAudioRecorder) private lazy var mockAudioSession: MockAudioSession! = .init() @@ -38,7 +38,7 @@ final class StreamAudioRecorderTests: XCTestCase { // MARK: - init - func testInitWithFilename_givenValidFilename_whenInitialized_thenSetsUpCorrectly() async throws { + @MainActor func testInitWithFilename_givenValidFilename_whenInitialized_thenSetsUpCorrectly() async throws { let filename = "test_recording.m4a" let recorder = StreamCallAudioRecorder(filename: filename) @@ -47,14 +47,14 @@ final class StreamAudioRecorderTests: XCTestCase { XCTAssertTrue(recorder.audioSession === AVAudioSession.sharedInstance()) } - func testInitWithBuilderAndSession_givenCustomBuilderAndSession_whenInitialized_thenUsesProvidedObjects() { + @MainActor func testInitWithBuilderAndSession_givenCustomBuilderAndSession_whenInitialized_thenUsesProvidedObjects() { XCTAssertTrue(subject.audioRecorderBuilder === builder) XCTAssertTrue(subject.audioSession === mockAudioSession) } // MARK: - deinit - func testFileDeletion_givenRecordingExists_whenDeinitialized_thenDeletesFile() async throws { + @MainActor func testFileDeletion_givenRecordingExists_whenDeinitialized_thenDeletesFile() async throws { let tempDirectory = FileManager.default.temporaryDirectory let filename = tempDirectory.appendingPathComponent("test_recording.m4a") @@ -72,7 +72,7 @@ final class StreamAudioRecorderTests: XCTestCase { // MARK: - startRecording - func testStartRecording_givenPermissionNotGranted_whenStarted_thenRecordsAndMetersAreNotUpdated() async throws { + @MainActor func testStartRecording_givenPermissionNotGranted_whenStarted_thenRecordsAndMetersAreNotUpdated() async throws { mockAudioSession.recordPermission = false await setUpHasActiveCall(true) @@ -81,7 +81,7 @@ final class StreamAudioRecorderTests: XCTestCase { try await assertRecording(false, isMeteringEnabled: false) } - func testStartRecording_givenPermissionGranted_whenStarted_thenRecordsAndMetersUpdates() async throws { + @MainActor func testStartRecording_givenPermissionGranted_whenStarted_thenRecordsAndMetersUpdates() async throws { mockAudioSession.recordPermission = true await setUpHasActiveCall(true) @@ -90,6 +90,7 @@ final class StreamAudioRecorderTests: XCTestCase { try await assertRecording(true) } + @MainActor func testStartRecording_givenPermissionGrantedButNoActiveCall_whenStarted_thenRecordsAndMetersWontStart() async throws { mockAudioSession.recordPermission = true @@ -98,7 +99,7 @@ final class StreamAudioRecorderTests: XCTestCase { try await assertRecording(false, isMeteringEnabled: false) } - func testStartRecording_givenPermissionGrantedButNoActiveCall_whenIgnoreActiveCallAndStarted_thenRecordsAndMetersUpdates( + @MainActor func testStartRecording_givenPermissionGrantedButNoActiveCall_whenIgnoreActiveCallAndStarted_thenRecordsAndMetersUpdates( ) async throws { mockAudioSession.recordPermission = true @@ -109,7 +110,7 @@ final class StreamAudioRecorderTests: XCTestCase { // MARK: - stopRecording - func testStopRecording_givenRecording_whenStopped_thenStopsRecording() async throws { + @MainActor func testStopRecording_givenRecording_whenStopped_thenStopsRecording() async throws { mockAudioSession.recordPermission = true await setUpHasActiveCall(true) @@ -121,7 +122,7 @@ final class StreamAudioRecorderTests: XCTestCase { // MARK: - activeCall ended - func test_activeCallEnded_givenAnActiveCallAndRecordingTrue_whenActiveCallEnds_thenStopsRecording() async throws { + @MainActor func test_activeCallEnded_givenAnActiveCallAndRecordingTrue_whenActiveCallEnds_thenStopsRecording() async throws { mockAudioSession.recordPermission = true await setUpHasActiveCall(true) await subject.startRecording() @@ -133,7 +134,7 @@ final class StreamAudioRecorderTests: XCTestCase { // MARK: - activeCall ended and a new one started - func test_activeCallEnded_givenAnActiveCallAndRecordingTrue_whenActiveCallEndsAndAnotherOneStarts_thenStartsRecording( + @MainActor func test_activeCallEnded_givenAnActiveCallAndRecordingTrue_whenActiveCallEndsAndAnotherOneStarts_thenStartsRecording( ) async throws { mockAudioSession.recordPermission = true await setUpHasActiveCall(true) @@ -176,7 +177,7 @@ final class StreamAudioRecorderTests: XCTestCase { ) } - private func setUpHasActiveCall( + @MainActor private func setUpHasActiveCall( _ hasActiveCall: Bool, timeout: TimeInterval = 0.5 ) async { diff --git a/StreamVideoTests/WebRTC/WebRTCClient_Tests.swift b/StreamVideoTests/WebRTC/WebRTCClient_Tests.swift index c8ffcc201..85bf8bd81 100644 --- a/StreamVideoTests/WebRTC/WebRTCClient_Tests.swift +++ b/StreamVideoTests/WebRTC/WebRTCClient_Tests.swift @@ -3,7 +3,7 @@ // @testable import StreamVideo -@preconcurrency import StreamWebRTC +import StreamWebRTC import XCTest final class WebRTCClient_Tests: StreamVideoTestCase { @@ -641,7 +641,7 @@ final class WebRTCClient_Tests: StreamVideoTestCase { XCTAssert(webRTCClient.publisher?.pendingIceCandidates.count == 1) } - func test_webRTCClient_changePublishQuality() async throws { + @MainActor func test_webRTCClient_changePublishQuality() async throws { // Given webRTCClient = makeWebRTCClient() try await test_webRTCClient_connectionFlow() diff --git a/StreamVideoTests/WebSocketClient/EventNotificationCenter_Tests.swift b/StreamVideoTests/WebSocketClient/EventNotificationCenter_Tests.swift index c60fdc22a..7e2fb451e 100644 --- a/StreamVideoTests/WebSocketClient/EventNotificationCenter_Tests.swift +++ b/StreamVideoTests/WebSocketClient/EventNotificationCenter_Tests.swift @@ -145,7 +145,7 @@ final class EventNotificationCenter_Tests: XCTestCase { let testEvent = TestEvent() // Setup event observer - var observerTriggered = false + nonisolated(unsafe) var observerTriggered = false let observer = center.addObserver( forName: .NewEventReceived, From ea9e4546d73aa34ec411da3b951e8ab829986d3f Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Tue, 23 Jul 2024 18:29:48 +0200 Subject: [PATCH 15/21] PR remarks --- Sources/StreamVideo/WebRTC/AudioSession.swift | 8 +++++--- .../WebSockets/Client/ConnectionRecoveryHandler.swift | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/StreamVideo/WebRTC/AudioSession.swift b/Sources/StreamVideo/WebRTC/AudioSession.swift index eeefd95a9..2685c6780 100644 --- a/Sources/StreamVideo/WebRTC/AudioSession.swift +++ b/Sources/StreamVideo/WebRTC/AudioSession.swift @@ -45,9 +45,9 @@ actor AudioSession { } nonisolated private func cleanup() { - RTCAudioSession.sharedInstance().lockForConfiguration() - RTCAudioSession.sharedInstance().isAudioEnabled = false - RTCAudioSession.sharedInstance().unlockForConfiguration() + rtcAudioSession.lockForConfiguration() + rtcAudioSession.isAudioEnabled = false + rtcAudioSession.unlockForConfiguration() } } @@ -62,3 +62,5 @@ extension RTCAudioSessionConfiguration: @unchecked Sendable { return configuration }() } + +extension RTCAudioSession: @unchecked Sendable {} diff --git a/Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift b/Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift index 2b49f219d..7484b8387 100644 --- a/Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift +++ b/Sources/StreamVideo/WebSockets/Client/ConnectionRecoveryHandler.swift @@ -191,6 +191,7 @@ private extension DefaultConnectionRecoveryHandler { // MARK: - Reconnection private extension DefaultConnectionRecoveryHandler { + // TODO: improve with reconnection refactor. @MainActor func reconnectIfNeeded() { guard canReconnectAutomatically else { return } From 02dd40beb3c6187944e71ac46123d44d7f49de83 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Tue, 23 Jul 2024 18:44:53 +0200 Subject: [PATCH 16/21] PR remarks --- .../Sources/Components/CodeScanner/CodeScanner.swift | 8 ++++++-- Sources/StreamVideo/Controllers/CallController.swift | 10 ++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift index 5b4f322b8..2d224f634 100644 --- a/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift +++ b/DemoApp/Sources/Components/CodeScanner/CodeScanner.swift @@ -213,7 +213,9 @@ final class ScannerViewController: UIViewController, UINavigationControllerDeleg reset() if (captureSession.isRunning == false) { - self.captureSession?.startRunning() + Task { + self.captureSession?.startRunning() + } } } @@ -319,7 +321,9 @@ final class ScannerViewController: UIViewController, UINavigationControllerDeleg super.viewDidDisappear(animated) if (captureSession?.isRunning == true) { - captureSession?.stopRunning() + Task { + captureSession?.stopRunning() + } } NotificationCenter.default.removeObserver(self) diff --git a/Sources/StreamVideo/Controllers/CallController.swift b/Sources/StreamVideo/Controllers/CallController.swift index f8a0b8e03..d4ff7c750 100644 --- a/Sources/StreamVideo/Controllers/CallController.swift +++ b/Sources/StreamVideo/Controllers/CallController.swift @@ -465,18 +465,16 @@ class CallController: @unchecked Sendable { private func handleParticipantsUpdated() { webRTCClient?.onParticipantsUpdated = { [weak self] participants in - guard let self else { return } - Task { @MainActor in - self.call?.state.participantsMap = participants + Task { @MainActor [weak self] in + self?.call?.state.participantsMap = participants } } } private func handleParticipantCountUpdated() { webRTCClient?.onParticipantCountUpdated = { [weak self] participantCount in - guard let self else { return } - Task { @MainActor in - self.call?.state.participantCount = participantCount + Task { @MainActor [weak self] in + self?.call?.state.participantCount = participantCount } } } From 35809baa446e3bebd717c30f33ffff986f908809 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Thu, 25 Jul 2024 13:38:57 +0200 Subject: [PATCH 17/21] PR remarks --- .../Components/Chat/DemoChatViewFactory.swift | 2 +- .../WebRTC/StreamVideoCaptureHandler.swift | 12 +--- .../VideoRenderer/VideoRendererView.swift | 32 +++------ .../StreamVideoSwiftUI/CallViewModel.swift | 10 ++- .../StreamPictureInPictureVideoRenderer.swift | 69 +++++++++---------- 5 files changed, 53 insertions(+), 72 deletions(-) diff --git a/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift b/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift index 490691725..34dea2a37 100644 --- a/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift +++ b/DemoApp/Sources/Components/Chat/DemoChatViewFactory.swift @@ -16,7 +16,7 @@ final class DemoChatViewFactory { @MainActor static let shared = DemoChatViewFactory() - func makeReactionsOverlayView( + @MainActor func makeReactionsOverlayView( channel: ChatChannel, currentSnapshot: UIImage, messageDisplayInfo: MessageDisplayInfo, diff --git a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift index da3a4f555..7e144775f 100644 --- a/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift +++ b/Sources/StreamVideo/WebRTC/StreamVideoCaptureHandler.swift @@ -76,8 +76,8 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { } @objc private func updateRotation() { - Task { @MainActor in - self.sceneOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .unknown + Task { @MainActor [weak self] in + self?.sceneOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .unknown } } @@ -127,13 +127,7 @@ final class StreamVideoCaptureHandler: NSObject, RTCVideoCapturerDelegate { } deinit { - if let notification { - NotificationCenter.default.removeObserver( - self, - name: notification, - object: nil - ) - } + NotificationCenter.default.removeObserver(self) } } diff --git a/Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift b/Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift index 2b3b74e2d..fb7006bb7 100644 --- a/Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift +++ b/Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift @@ -5,6 +5,7 @@ import Combine import Foundation import StreamVideo +import StreamWebRTC import SwiftUI /// A view that wraps a `VideoRenderer` and integrates with SwiftUI. @@ -52,17 +53,6 @@ public struct VideoRendererView: UIViewRepresentable { self.contentMode = contentMode } - /// Dismantles the `UIView` when it is no longer needed. - /// - Parameters: - /// - uiView: The `VideoRenderer` to dismantle. - /// - coordinator: The coordinator associated with the view. - public static func dismantleUIView( - _ uiView: VideoRenderer, - coordinator: Coordinator - ) { - coordinator.dismantle() - } - /// Creates the `VideoRenderer` view. /// - Parameter context: The context containing information about the current state of the system. /// - Returns: A configured `VideoRenderer` instance. @@ -100,7 +90,7 @@ public struct VideoRendererView: UIViewRepresentable { /// Extension for `VideoRendererView` to define the `Coordinator` class. extension VideoRendererView { /// A class to coordinate the `VideoRendererView` and manage its lifecycle. - public final class Coordinator { + @MainActor public final class Coordinator { /// Injected dependency for accessing the video renderer pool. @Injected(\.videoRendererPool) private var videoRendererPool @@ -112,24 +102,24 @@ extension VideoRendererView { /// The video renderer managed by this coordinator. fileprivate private(set) lazy var renderer: VideoRenderer = videoRendererPool .acquireRenderer(size: .zero) + + nonisolated(unsafe) private var dismantle: (() -> Void)? /// Initializes a new instance of the coordinator. /// - Parameter handleRendering: A closure to handle the rendering of the video. init(handleRendering: ((VideoRenderer) -> Void)?) { self.handleRendering = handleRendering - _ = renderer setupRendererObservation() + dismantle = { [weak self] in + guard let self else { return } + renderer.track?.remove(renderer) + disposableBag.removeAll() + videoRendererPool.releaseRenderer(renderer) + } } deinit { - dismantle() - } - - /// Dismantles the video renderer and releases resources. - func dismantle() { - renderer.track?.remove(renderer) - disposableBag.removeAll() - videoRendererPool.releaseRenderer(renderer) + dismantle?() } // MARK: Private API diff --git a/Sources/StreamVideoSwiftUI/CallViewModel.swift b/Sources/StreamVideoSwiftUI/CallViewModel.swift index f2b483bfb..0c3117ca5 100644 --- a/Sources/StreamVideoSwiftUI/CallViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallViewModel.swift @@ -552,9 +552,8 @@ open class CallViewModel: ObservableObject { callingState = .idle isMinimized = false localVideoPrimary = false - let audioRecorder = self.audioRecorder - Task { - await audioRecorder.stopRecording() + Task { [weak self] in + await self?.audioRecorder.stopRecording() } } @@ -600,9 +599,8 @@ open class CallViewModel: ObservableObject { log.error("Error starting a call", error: error) self.error = error callingState = .idle - let audioRecorder = self.audioRecorder - Task { - await audioRecorder.stopRecording() + Task { [weak self] in + await self?.audioRecorder.stopRecording() } enteringCallTask = nil } diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift index 36b2b2ad1..661f24005 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureVideoRenderer.swift @@ -2,10 +2,10 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // -import Combine +@preconcurrency import Combine import Foundation import StreamVideo -@preconcurrency import StreamWebRTC +import StreamWebRTC /// A view that can be used to render an instance of `RTCVideoTrack` final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { @@ -30,7 +30,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { var pictureInPictureWindowSizePolicy: PictureInPictureWindowSizePolicy /// The publisher which is used to streamline the frames received from the track. - private let bufferPublisher: PassthroughSubject = .init() + nonisolated(unsafe) private let bufferPublisher: PassthroughSubject = .init() /// The view that contains the rendering layer. private lazy var contentView: SampleBufferVideoCallView = { @@ -43,7 +43,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { }() /// The transformer used to transform and downsample a RTCVideoFrame's buffer. - private var bufferTransformer = StreamBufferTransformer() + nonisolated(unsafe) private var bufferTransformer = StreamBufferTransformer() /// The cancellable used to control the bufferPublisher stream. private var bufferUpdatesCancellable: AnyCancellable? @@ -51,13 +51,15 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { /// The view's size. /// - Note: We are using this property instead for `frame.size` or `bounds.size` so we can /// access it from any thread. - private var contentSize: CGSize = .zero + nonisolated(unsafe) private var contentSize: CGSize = .zero /// The track's size. - private var trackSize: CGSize = .zero { + nonisolated(unsafe) private var trackSize: CGSize = .zero { didSet { guard trackSize != oldValue else { return } - didUpdateTrackSize() + Task { @MainActor in + didUpdateTrackSize() + } } } @@ -71,14 +73,14 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { /// to improve performance. /// - Note: The number of frames to skip is being calculated based on the ``trackSize`` and /// ``contentSize``. It takes into account also the ``sizeRatioThreshold`` - private var noOfFramesToSkipAfterRendering = 1 + nonisolated(unsafe) private var noOfFramesToSkipAfterRendering = 1 /// The number of frames that we have skipped so far. This is used as a step counter in the /// ``renderFrame(_:)``. - private var skippedFrames = 0 + nonisolated(unsafe) private var skippedFrames = 0 /// We render frames every time the stepper/counter value is 0 and have a valid trackSize. - private var shouldRenderFrame: Bool { skippedFrames == 0 && trackSize != .zero } + nonisolated(unsafe) private var shouldRenderFrame: Bool { skippedFrames == 0 && trackSize != .zero } /// A size ratio threshold used to determine if resizing is required. /// - Note: It seems that Picture-in-Picture doesn't like rendering frames that are bigger than its @@ -125,35 +127,32 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { } nonisolated func renderFrame(_ frame: RTCVideoFrame?) { - Task { @MainActor in - guard let frame = frame else { - return - } + guard let frame = frame else { + return + } - // Update the trackSize and re-calculate rendering properties if the size - // has changed. - trackSize = .init(width: Int(frame.width), height: Int(frame.height)) + // Update the trackSize and re-calculate rendering properties if the size + // has changed. + trackSize = .init(width: Int(frame.width), height: Int(frame.height)) - log.debug("→ Received frame with trackSize:\(trackSize)") + log.debug("→ Received frame with trackSize:\(trackSize)") - defer { - handleFrameSkippingIfRequired() - } + defer { + handleFrameSkippingIfRequired() + } - guard shouldRenderFrame else { - log.debug("→ Skipping frame.") - return - } + guard shouldRenderFrame else { + log.debug("→ Skipping frame.") + return + } - if - let yuvBuffer = bufferTransformer.transformAndResizeIfRequired(frame, targetSize: contentSize)? - .buffer as? StreamRTCYUVBuffer, - let sampleBuffer = yuvBuffer.sampleBuffer { - log.debug("➕ Buffer for trackId:\(track?.trackId ?? "n/a") added.") - bufferPublisher.send(sampleBuffer) - } else { - log.warning("Failed to convert \(type(of: frame.buffer)) CMSampleBuffer.") - } + if + let yuvBuffer = bufferTransformer.transformAndResizeIfRequired(frame, targetSize: contentSize)? + .buffer as? StreamRTCYUVBuffer, + let sampleBuffer = yuvBuffer.sampleBuffer { + bufferPublisher.send(sampleBuffer) + } else { + log.warning("Failed to convert \(type(of: frame.buffer)) CMSampleBuffer.") } } @@ -258,7 +257,7 @@ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer { } /// A method used to handle the frameSkipping(step) during frame consumption. - private func handleFrameSkippingIfRequired() { + nonisolated private func handleFrameSkippingIfRequired() { if noOfFramesToSkipAfterRendering > 0 { if skippedFrames == noOfFramesToSkipAfterRendering { skippedFrames = 0 From 2ae50317194bd032b63993ecab9c630eb77e35fb Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Fri, 26 Jul 2024 15:52:11 +0200 Subject: [PATCH 18/21] Fixed some warnings --- .../Stages/StreamCallStateMachine+AcceptedStage.swift | 2 +- .../Stages/StreamCallStateMachine+ErrorStage.swift | 2 +- .../Stages/StreamCallStateMachine+IdleStage.swift | 2 +- .../Stages/StreamCallStateMachine+JoinedStage.swift | 2 +- .../Stages/StreamCallStateMachine+RejectedStage.swift | 2 +- .../WebRTC/Screensharing/BroadcastBufferReaderConnection.swift | 2 +- .../BlurBackgroundFilter/BlurBackgroundVideoFilter.swift | 2 +- .../ImageBackgroundFilter/ImageBackgroundVideoFilter.swift | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+AcceptedStage.swift b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+AcceptedStage.swift index c05606c78..de7576588 100644 --- a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+AcceptedStage.swift +++ b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+AcceptedStage.swift @@ -26,7 +26,7 @@ extension StreamCallStateMachine.Stage { extension StreamCallStateMachine.Stage { /// A class representing the accepted stage in the `StreamCallStateMachine`. - final class AcceptedStage: StreamCallStateMachine.Stage { + final class AcceptedStage: StreamCallStateMachine.Stage, @unchecked Sendable { let response: AcceptCallResponse /// Initializes a new accepted stage with the provided call and response. diff --git a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+ErrorStage.swift b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+ErrorStage.swift index fc538ce79..741ae286d 100644 --- a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+ErrorStage.swift +++ b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+ErrorStage.swift @@ -26,7 +26,7 @@ extension StreamCallStateMachine.Stage { extension StreamCallStateMachine.Stage { /// A class representing the error stage in the `StreamCallStateMachine`. - final class ErrorStage: StreamCallStateMachine.Stage { + final class ErrorStage: StreamCallStateMachine.Stage, @unchecked Sendable { let error: Error /// Initializes a new error stage with the provided call and error. diff --git a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+IdleStage.swift b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+IdleStage.swift index 508557faf..d0eeddb6f 100644 --- a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+IdleStage.swift +++ b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+IdleStage.swift @@ -18,7 +18,7 @@ extension StreamCallStateMachine.Stage { extension StreamCallStateMachine.Stage { /// A class representing the idle stage in the `StreamCallStateMachine`. - final class IdleStage: StreamCallStateMachine.Stage { + final class IdleStage: StreamCallStateMachine.Stage, @unchecked Sendable { /// Initializes a new idle stage with the provided call. /// diff --git a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+JoinedStage.swift b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+JoinedStage.swift index 60ebc54b0..9f3b4470b 100644 --- a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+JoinedStage.swift +++ b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+JoinedStage.swift @@ -26,7 +26,7 @@ extension StreamCallStateMachine.Stage { extension StreamCallStateMachine.Stage { /// A class representing the joined stage in the `StreamCallStateMachine`. - final class JoinedStage: StreamCallStateMachine.Stage { + final class JoinedStage: StreamCallStateMachine.Stage, @unchecked Sendable { let response: JoinCallResponse /// Initializes a new joined stage with the provided call and response. diff --git a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+RejectedStage.swift b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+RejectedStage.swift index d95c983b8..721efc84a 100644 --- a/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+RejectedStage.swift +++ b/Sources/StreamVideo/Utils/StateMachine/CallStateMachine/Stages/StreamCallStateMachine+RejectedStage.swift @@ -24,7 +24,7 @@ extension StreamCallStateMachine.Stage { extension StreamCallStateMachine.Stage { /// A class representing the rejected stage in the `StreamCallStateMachine`. - final class RejectedStage: StreamCallStateMachine.Stage { + final class RejectedStage: StreamCallStateMachine.Stage, @unchecked Sendable { let response: RejectCallResponse /// Initializes a new rejected stage with the provided call and response. diff --git a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferReaderConnection.swift b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferReaderConnection.swift index bffb70610..84e3938d3 100644 --- a/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferReaderConnection.swift +++ b/Sources/StreamVideo/WebRTC/Screensharing/BroadcastBufferReaderConnection.swift @@ -5,7 +5,7 @@ import Darwin import Foundation -final class BroadcastBufferReaderConnection: BroadcastBufferConnection { +final class BroadcastBufferReaderConnection: BroadcastBufferConnection, @unchecked Sendable { private let streamDelegate: StreamDelegate private let filePath: String diff --git a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/BlurBackgroundFilter/BlurBackgroundVideoFilter.swift b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/BlurBackgroundFilter/BlurBackgroundVideoFilter.swift index 153502665..47c413b6b 100644 --- a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/BlurBackgroundFilter/BlurBackgroundVideoFilter.swift +++ b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/BlurBackgroundFilter/BlurBackgroundVideoFilter.swift @@ -14,7 +14,7 @@ import Foundation /// /// This filter is available on iOS 15.0 and later. @available(iOS 15.0, *) -public final class BlurBackgroundVideoFilter: VideoFilter { +public final class BlurBackgroundVideoFilter: VideoFilter, @unchecked Sendable { private let backgroundImageFilterProcessor = BackgroundImageFilterProcessor() diff --git a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/ImageBackgroundFilter/ImageBackgroundVideoFilter.swift b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/ImageBackgroundFilter/ImageBackgroundVideoFilter.swift index 22b110c3b..34b523eb5 100644 --- a/Sources/StreamVideo/WebRTC/VideoFilters/Filters/ImageBackgroundFilter/ImageBackgroundVideoFilter.swift +++ b/Sources/StreamVideo/WebRTC/VideoFilters/Filters/ImageBackgroundFilter/ImageBackgroundVideoFilter.swift @@ -11,7 +11,7 @@ import Foundation /// the foreground objects using a filter processor. It caches processed background images to optimize /// performance for matching input sizes and orientations. @available(iOS 15.0, *) -public final class ImageBackgroundVideoFilter: VideoFilter { +public final class ImageBackgroundVideoFilter: VideoFilter, @unchecked Sendable { private struct CacheValue: Hashable { var originalImageSize: CGSize From da5fd908baa31341f068f6604f6ca589f6411995 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Tue, 30 Jul 2024 17:52:42 +0200 Subject: [PATCH 19/21] Changed the smoke checks Xcode version to 15.3 --- .github/workflows/smoke-checks.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke-checks.yml b/.github/workflows/smoke-checks.yml index e3258006e..587e5a090 100644 --- a/.github/workflows/smoke-checks.yml +++ b/.github/workflows/smoke-checks.yml @@ -51,8 +51,8 @@ jobs: run: bundle exec fastlane test device:"${{ env.IOS_SIMULATOR_DEVICE }}" timeout-minutes: 40 env: - XCODE_VERSION: "15.4" # the most stable pair of Xcode - IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" # and iOS + XCODE_VERSION: "15.3" # the most stable pair of Xcode + IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.3)" # and iOS - name: Get branch name id: get_branch_name run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT @@ -108,8 +108,8 @@ jobs: run: bundle exec fastlane test_swiftui device:"${{ env.IOS_SIMULATOR_DEVICE }}" record:${{ github.event.inputs.swiftui_snapshots }} timeout-minutes: 40 env: - XCODE_VERSION: "15.4" # the most stable pair of Xcode - IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" # and iOS + XCODE_VERSION: "15.3" # the most stable pair of Xcode + IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.3)" # and iOS - name: Parse xcresult if: failure() run: | @@ -141,8 +141,8 @@ jobs: run: bundle exec fastlane test_uikit device:"${{ env.IOS_SIMULATOR_DEVICE }}" record:${{ github.event.inputs.uikit_snapshots }} timeout-minutes: 40 env: - XCODE_VERSION: "15.4" # the most stable pair of Xcode - IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" # and iOS + XCODE_VERSION: "15.3" # the most stable pair of Xcode + IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.3)" # and iOS - name: Parse xcresult if: failure() run: | @@ -309,8 +309,8 @@ jobs: run: bundle exec fastlane test_e2e device:"${{ env.IOS_SIMULATOR_DEVICE }}" batch:'${{ matrix.batch }}' test_without_building:true timeout-minutes: 60 env: - XCODE_VERSION: "15.4" # the most stable pair of Xcode - IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.4)" # and iOS + XCODE_VERSION: "15.3" # the most stable pair of Xcode + IOS_SIMULATOR_DEVICE: "iPhone 15 Pro (17.3)" # and iOS MATRIX_SIZE: ${{ strategy.job-total }} STREAM_SDK_TEST_APP: ${{ secrets.STREAM_SDK_TEST_APP }} STREAM_SDK_TEST_ACCOUNT_EMAIL: ${{ secrets.STREAM_SDK_TEST_ACCOUNT_EMAIL }} From 3f1bc36f9788c6384ea46509e1cc3c371f8b8011 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Tue, 30 Jul 2024 19:37:11 +0200 Subject: [PATCH 20/21] Some fixes --- .../DependencyInjection/InjectedValues.swift | 14 ++++++++++++++ .../CallView/CallDurationView.swift | 5 ++++- .../CallView/ParticipantsGridLayout.swift | 2 +- .../CallView/ScreenSharing/ScreenSharingView.swift | 2 +- .../CallView/VideoRenderer/VideoRendererView.swift | 4 +++- Sources/StreamVideoSwiftUI/CallViewModel.swift | 5 ++++- .../CallingViews/iOS13/BackportStateObject.swift | 9 ++++++--- .../StreamDeviceOrientationAdapter.swift | 8 ++++---- .../Utils/Formatters/Formatters.swift | 4 ++-- ...AVPictureInPictureVideoCallViewController.swift | 2 +- .../StreamPictureInPictureAdapter.swift | 4 ++-- .../StreamPictureInPictureController.swift | 12 ++++++++---- .../StreamPictureInPictureView.swift | 4 +++- .../StreamPictureInPictureWindowSizePolicy.swift | 2 +- .../VideoRendererPool/VideoRendererPool.swift | 4 ++-- 15 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Sources/StreamVideo/DependencyInjection/InjectedValues.swift b/Sources/StreamVideo/DependencyInjection/InjectedValues.swift index 48876600e..7493bea81 100644 --- a/Sources/StreamVideo/DependencyInjection/InjectedValues.swift +++ b/Sources/StreamVideo/DependencyInjection/InjectedValues.swift @@ -12,6 +12,15 @@ public protocol InjectionKey { static var currentValue: Self.Value { get set } } +@MainActor +public protocol InjectionKeyMainActor { + /// The associated type representing the type of the dependency injection key's value. + associatedtype Value + + /// The default value for the dependency injection key. + static var currentValue: Self.Value { get set } +} + /// Provides access to injected dependencies. public struct InjectedValues { /// This is only used as an accessor to the computed properties within extensions of `InjectedValues`. @@ -23,6 +32,11 @@ public struct InjectedValues { set { key.currentValue = newValue } } + @MainActor public static subscript(key: K.Type) -> K.Value where K: InjectionKeyMainActor { + get { key.currentValue } + set { key.currentValue = newValue } + } + /// A static subscript accessor for updating and references dependencies directly. public static subscript(_ keyPath: WritableKeyPath) -> T { get { current[keyPath: keyPath] } diff --git a/Sources/StreamVideoSwiftUI/CallView/CallDurationView.swift b/Sources/StreamVideoSwiftUI/CallView/CallDurationView.swift index 17f592634..8d8d76fe4 100644 --- a/Sources/StreamVideoSwiftUI/CallView/CallDurationView.swift +++ b/Sources/StreamVideoSwiftUI/CallView/CallDurationView.swift @@ -12,7 +12,10 @@ public struct CallDurationView: View { @Injected(\.colors) private var colors: Colors @Injected(\.fonts) private var fonts: Fonts @Injected(\.images) private var images: Images - @Injected(\.formatters.mediaDuration) private var formatter: MediaDurationFormatter + + @MainActor var formatter: MediaDurationFormatter { + InjectedValues[\.formatters.mediaDuration] + } @State private var duration: TimeInterval @ObservedObject private var viewModel: CallViewModel diff --git a/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift b/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift index a1d85b273..83eab3388 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ParticipantsGridLayout.swift @@ -14,7 +14,7 @@ public struct ParticipantsGridLayout: View { var availableFrame: CGRect var onChangeTrackVisibility: @MainActor(CallParticipant, Bool) -> Void - @ObservedObject private var orientationAdapter = InjectedValues[\.orientationAdapter] + @ObservedObject private var orientationAdapter = MainActor.assumeIsolated { InjectedValues[\.orientationAdapter] } public init( viewFactory: Factory, diff --git a/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift b/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift index 2f84335dc..c1ff22bc1 100644 --- a/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift +++ b/Sources/StreamVideoSwiftUI/CallView/ScreenSharing/ScreenSharingView.swift @@ -17,7 +17,7 @@ public struct ScreenSharingView: View { var isZoomEnabled: Bool private let identifier = UUID() - @ObservedObject private var orientationAdapter = InjectedValues[\.orientationAdapter] + @ObservedObject private var orientationAdapter = MainActor.assumeIsolated { InjectedValues[\.orientationAdapter] } public init( viewModel: CallViewModel, diff --git a/Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift b/Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift index fb7006bb7..108af064a 100644 --- a/Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift +++ b/Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift @@ -92,7 +92,9 @@ extension VideoRendererView { /// A class to coordinate the `VideoRendererView` and manage its lifecycle. @MainActor public final class Coordinator { /// Injected dependency for accessing the video renderer pool. - @Injected(\.videoRendererPool) private var videoRendererPool + private var videoRendererPool: VideoRendererPool { + InjectedValues[\.videoRendererPool] + } /// A closure to handle the rendering of the video. private let handleRendering: ((VideoRenderer) -> Void)? diff --git a/Sources/StreamVideoSwiftUI/CallViewModel.swift b/Sources/StreamVideoSwiftUI/CallViewModel.swift index 0c3117ca5..765c3ae39 100644 --- a/Sources/StreamVideoSwiftUI/CallViewModel.swift +++ b/Sources/StreamVideoSwiftUI/CallViewModel.swift @@ -12,8 +12,11 @@ import SwiftUI open class CallViewModel: ObservableObject { @Injected(\.streamVideo) var streamVideo - @Injected(\.pictureInPictureAdapter) var pictureInPictureAdapter @Injected(\.callAudioRecorder) var audioRecorder + + @MainActor var pictureInPictureAdapter: StreamPictureInPictureAdapter { + InjectedValues[\.pictureInPictureAdapter] + } /// Provides access to the current call. @Published public private(set) var call: Call? { diff --git a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift index 3b7ee99ce..4c4102212 100644 --- a/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift +++ b/Sources/StreamVideoSwiftUI/CallingViews/iOS13/BackportStateObject.swift @@ -42,10 +42,13 @@ public struct BackportStateObject self.thunk = thunk } - public mutating func update() { + nonisolated public mutating func update() { + guard Thread.isMainThread else { return } // Not sure what this does but we'll just forward it - _state.update() - _observedObject.update() + MainActor.assumeIsolated { + _state.update() + _observedObject.update() + } } } diff --git a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift index 9fed28e90..3e9e866ee 100644 --- a/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift +++ b/Sources/StreamVideoSwiftUI/Utils/DeviceOrientation/StreamDeviceOrientationAdapter.swift @@ -2,7 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // -@preconcurrency import Combine +import Combine import Foundation import StreamVideo #if canImport(UIKit) @@ -83,14 +83,14 @@ enum StreamDeviceOrientationAdapterKey: @preconcurrency InjectionKey { @MainActor static var currentValue: StreamDeviceOrientationAdapter = .init() } #else -enum StreamDeviceOrientationAdapterKey: InjectionKey { - static var currentValue: StreamDeviceOrientationAdapter = .init() +enum StreamDeviceOrientationAdapterKey: InjectionKeyMainActor { + @MainActor static var currentValue: StreamDeviceOrientationAdapter = .init() } #endif extension InjectedValues { /// Provides access to the `StreamDeviceOrientationAdapter` class to the views and view models. - public var orientationAdapter: StreamDeviceOrientationAdapter { + @MainActor public var orientationAdapter: StreamDeviceOrientationAdapter { get { Self[StreamDeviceOrientationAdapterKey.self] } diff --git a/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift b/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift index d13964d20..6e2ded9f0 100644 --- a/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift +++ b/Sources/StreamVideoSwiftUI/Utils/Formatters/Formatters.swift @@ -17,14 +17,14 @@ enum FormattersKey: @preconcurrency InjectionKey { @MainActor static var currentValue: Formatters = .init() } #else -enum FormattersKey: InjectionKey { +enum FormattersKey: InjectionKeyMainActor { @MainActor static var currentValue: Formatters = .init() } #endif extension InjectedValues { /// Provides access to the `Formatters` class to the views and view models. - public var formatters: Formatters { + @MainActor public var formatters: Formatters { get { Self[FormattersKey.self] } diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift index 185361dd4..7ed26e73e 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift @@ -8,7 +8,7 @@ import StreamVideo import StreamWebRTC /// Describes an object that can be used to present picture-in-picture content. -protocol StreamAVPictureInPictureViewControlling: AnyObject { +@MainActor protocol StreamAVPictureInPictureViewControlling: AnyObject { /// The closure to call whenever the picture-in-picture window size changes. var onSizeUpdate: ((CGSize) -> Void)? { get set } diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift index 3c341a270..93b0ea85a 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureAdapter.swift @@ -99,14 +99,14 @@ enum StreamPictureInPictureAdapterKey: @preconcurrency InjectionKey { @MainActor static var currentValue: StreamPictureInPictureAdapter = .init() } #else -enum StreamPictureInPictureAdapterKey: InjectionKey { +enum StreamPictureInPictureAdapterKey: InjectionKeyMainActor { @MainActor static var currentValue: StreamPictureInPictureAdapter = .init() } #endif extension InjectedValues { /// Provides access to the `StreamPictureInPictureAdapter` class to the views and view models. - var pictureInPictureAdapter: StreamPictureInPictureAdapter { + @MainActor var pictureInPictureAdapter: StreamPictureInPictureAdapter { get { Self[StreamPictureInPictureAdapterKey.self] } diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureController.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureController.swift index 3b24b95f0..279d300e4 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureController.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureController.swift @@ -12,7 +12,7 @@ import UIKit #endif /// A controller class for picture-in-picture whenever that is possible. -final class StreamPictureInPictureController: NSObject, AVPictureInPictureControllerDelegate { +final class StreamPictureInPictureController: NSObject, AVPictureInPictureControllerDelegate, @unchecked Sendable { // MARK: - Properties @@ -33,7 +33,9 @@ final class StreamPictureInPictureController: NSObject, AVPictureInPictureContro /// A closure called when the picture-in-picture view's size changes. public var onSizeUpdate: ((CGSize) -> Void)? { didSet { - contentViewController?.onSizeUpdate = onSizeUpdate // Updates the onSizeUpdate closure of the content view controller + Task { @MainActor in + contentViewController?.onSizeUpdate = onSizeUpdate // Updates the onSizeUpdate closure of the content vc + } } } @@ -132,8 +134,10 @@ final class StreamPictureInPictureController: NSObject, AVPictureInPictureContro // MARK: - Private helpers private func didUpdate(_ track: RTCVideoTrack?) { - contentViewController?.track = track - trackStateAdapter.activeTrack = track + Task { @MainActor in + contentViewController?.track = track + trackStateAdapter.activeTrack = track + } } private func didUpdate(_ sourceView: UIView?) { diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureView.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureView.swift index 890a3e58a..87e3681a8 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureView.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/StreamPictureInPictureView.swift @@ -10,7 +10,9 @@ import SwiftUI /// very weird if the sourceView isn't in the ViewHierarchy or doesn't have an appropriate size. struct StreamPictureInPictureView: UIViewRepresentable { - @Injected(\.pictureInPictureAdapter) private var pictureInPictureAdapter + @MainActor private var pictureInPictureAdapter: StreamPictureInPictureAdapter { + InjectedValues[\.pictureInPictureAdapter] + } var isActive: Bool diff --git a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureWindowSizePolicy.swift b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureWindowSizePolicy.swift index 432efea88..1f03c7436 100644 --- a/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureWindowSizePolicy.swift +++ b/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureWindowSizePolicy.swift @@ -5,7 +5,7 @@ import Foundation /// Protocol defining the policy for determining the window size of a Picture-in-Picture (PiP) view. -protocol PictureInPictureWindowSizePolicy { +@MainActor protocol PictureInPictureWindowSizePolicy { /// The current size of the track to be displayed in the PiP window. var trackSize: CGSize { get set } diff --git a/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift b/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift index 4905090e9..f66c0392d 100644 --- a/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift +++ b/Sources/StreamVideoSwiftUI/Utils/VideoRendererPool/VideoRendererPool.swift @@ -52,13 +52,13 @@ extension VideoRendererPool: @preconcurrency InjectionKey { @MainActor static var currentValue: VideoRendererPool = .init() } #else -extension VideoRendererPool: InjectionKey { +extension VideoRendererPool: InjectionKeyMainActor { @MainActor static var currentValue: VideoRendererPool = .init() } #endif extension InjectedValues { - var videoRendererPool: VideoRendererPool { + @MainActor var videoRendererPool: VideoRendererPool { get { Self[VideoRendererPool.self] } set { _ = newValue } } From 0be8e173454be531aa147a51a04ff1e8e7d33285 Mon Sep 17 00:00:00 2001 From: Stream Bot Date: Tue, 30 Jul 2024 19:43:53 +0200 Subject: [PATCH 21/21] Fixed tests --- .../StreamPictureInPictureVideoRendererTests.swift | 2 +- .../StreamPictureInPictureAdaptiveWindowSizePolicy_Tests.swift | 2 +- .../StreamPictureInPictureFixedWindowSizePolicy_Tests.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureVideoRendererTests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureVideoRendererTests.swift index 060f61548..a4ca0e37f 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureVideoRendererTests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/StreamPictureInPictureVideoRendererTests.swift @@ -8,7 +8,7 @@ import XCTest final class StreamPictureInPictureVideoRenderer_Tests: XCTestCase { - func test_didUpdateTrackSize_windowSizePolicyWasUpdated() { + @MainActor func test_didUpdateTrackSize_windowSizePolicyWasUpdated() { let spyPolicy = StreamTestSpyPictureInPictureWindowSizePolicy() let subject = StreamPictureInPictureVideoRenderer(windowSizePolicy: spyPolicy) let targetSize = CGSize(width: 100, height: 150) diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureAdaptiveWindowSizePolicy_Tests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureAdaptiveWindowSizePolicy_Tests.swift index 7e530bede..1dab812a0 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureAdaptiveWindowSizePolicy_Tests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureAdaptiveWindowSizePolicy_Tests.swift @@ -19,7 +19,7 @@ final class StreamPictureInPictureAdaptiveWindowSizePolicy_Tests: XCTestCase { // MARK: - didSetTrackSize - func test_didSetTrackSize_setsPreferredContentSizeOnController() { + @MainActor func test_didSetTrackSize_setsPreferredContentSizeOnController() { let controller = MockStreamAVPictureInPictureViewControlling() subject.controller = controller diff --git a/StreamVideoSwiftUITests/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureFixedWindowSizePolicy_Tests.swift b/StreamVideoSwiftUITests/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureFixedWindowSizePolicy_Tests.swift index 167f78dfc..9c10100e0 100644 --- a/StreamVideoSwiftUITests/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureFixedWindowSizePolicy_Tests.swift +++ b/StreamVideoSwiftUITests/Utils/PictureInPicture/WindowSizePolicy/StreamPictureInPictureFixedWindowSizePolicy_Tests.swift @@ -19,7 +19,7 @@ final class StreamPictureInPictureFixedWindowSizePolicy_Tests: XCTestCase { // MARK: - didSetController - func test_didSetController_setsPreferredContentSizeOnController() { + @MainActor func test_didSetController_setsPreferredContentSizeOnController() { let controller = MockStreamAVPictureInPictureViewControlling() subject.controller = controller