From e61cbca4cfa638ebc50e3782830262d8f4dcc202 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Tue, 29 Oct 2024 23:32:46 +0200 Subject: [PATCH] Update AudioSession object --- .../StreamCallAudioRecorder.swift | 3 - Sources/StreamVideo/WebRTC/AudioSession.swift | 253 +++++++++++++++--- .../MediaAdapters/AudioMediaAdapter.swift | 24 -- .../LocalAudioMediaAdapter.swift | 79 +++--- .../MediaAdapters/MediaAdapter.swift | 24 -- .../RTCPeerConnectionCoordinator.swift | 46 ---- .../Stages/WebRTCCoordinator+Joined.swift | 9 +- .../WebRTC/v2/WebRTCStateAdapter.swift | 20 +- .../StreamVideoSwiftUI/Utils/ToastView.swift | 12 +- StreamVideo.xcodeproj/project.pbxproj | 12 +- .../WebRTC/AudioSession_Tests.swift | 61 +++++ .../AudioMediaAdapter_Tests.swift | 20 -- .../LocalAudioMediaAdapter_Tests.swift | 2 +- 13 files changed, 341 insertions(+), 224 deletions(-) create mode 100644 StreamVideoTests/WebRTC/AudioSession_Tests.swift diff --git a/Sources/StreamVideo/Utils/AudioRecorder/StreamCallAudioRecorder.swift b/Sources/StreamVideo/Utils/AudioRecorder/StreamCallAudioRecorder.swift index 3feace631..fa52f71c5 100644 --- a/Sources/StreamVideo/Utils/AudioRecorder/StreamCallAudioRecorder.swift +++ b/Sources/StreamVideo/Utils/AudioRecorder/StreamCallAudioRecorder.swift @@ -196,9 +196,6 @@ open class StreamCallAudioRecorder: @unchecked Sendable { } private func setUpAudioCaptureIfRequired() async throws -> AVAudioRecorder { - try audioSession.setCategory(.playAndRecord) - try audioSession.setActive(true, options: []) - guard await audioSession.requestRecordPermission() else { diff --git a/Sources/StreamVideo/WebRTC/AudioSession.swift b/Sources/StreamVideo/WebRTC/AudioSession.swift index 233c2a84b..e28c944d6 100644 --- a/Sources/StreamVideo/WebRTC/AudioSession.swift +++ b/Sources/StreamVideo/WebRTC/AudioSession.swift @@ -2,65 +2,233 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // +import AVFoundation +import Combine import Foundation import StreamWebRTC -extension RTCAudioSessionConfiguration: @unchecked Sendable {} +/// The `AudioSession` class manages the device's audio session for an application, +/// providing control over activation, mode configuration, and routing to speakers or in-ear speakers. +final class AudioSession { -actor AudioSession { - - private let rtcAudioSession: RTCAudioSession = RTCAudioSession.sharedInstance() + private let speakerQueue = UnfairQueue() + private var _isSpeakerOn: Bool = false + private var isSpeakerOn: Bool { + get { speakerQueue.sync { _isSpeakerOn } } + set { speakerQueue.sync { _isSpeakerOn = newValue } } + } + + private let isActiveQueue = UnfairQueue() + private var _isActive: Bool = false + var isActive: Bool { + get { isActiveQueue.sync { _isActive } } + set { isActiveQueue.sync { _isActive = newValue } } + } + + private let audioSession = RTCAudioSession.sharedInstance() + private var configuration = RTCAudioSessionConfiguration.default + + private var activeCallSettings: CallSettings? + private var routeChangeCancellable: AnyCancellable? + private let defaultCategoryOptions: AVAudioSession.CategoryOptions = [ + .allowBluetooth, + .allowBluetoothA2DP + ] + + weak var delegate: AudioSessionDelegate? + + init() { + audioSession.useManualAudio = true + audioSession.isAudioEnabled = true + configuration.categoryOptions = defaultCategoryOptions + configureRouteChangeListener() + do { + try audioSession.setCategory(.playAndRecord) + } catch { + log.error("Failed to set audio session category for playback and recording.", subsystems: .webRTC) + } + } + + func didUpdate(_ callSettings: CallSettings) throws { + guard callSettings != activeCallSettings else { return } + + if !isActive, callSettings.audioOutputOn { + let mode: AVAudioSession.Mode = callSettings.speakerOn ? .videoChat : .voiceChat + try activate(mode: mode) + try toggleSpeaker(callSettings.speakerOn) + } else if isActive, callSettings.audioOutputOn { + let mode: AVAudioSession.Mode = callSettings.speakerOn ? .videoChat : .voiceChat + try activate(mode: mode) + try toggleSpeaker(callSettings.speakerOn) + } else if isActive, !callSettings.audioOutputOn { + try deactivate() + } else { + /* No-op */ + } - var isActive: Bool { rtcAudioSession.isActive } - var isAudioEnabled: Bool { rtcAudioSession.isAudioEnabled } - var isSpeakerOn: Bool { rtcAudioSession.categoryOptions.contains(.defaultToSpeaker) } + activeCallSettings = callSettings + log.debug( + "AudioSession updated with \(callSettings).", + subsystems: .webRTC + ) + } + + private func activate(mode: AVAudioSession.Mode) throws { + audioSession.lockForConfiguration() + defer { audioSession.unlockForConfiguration() } + configuration.mode = mode.rawValue + try audioSession.setConfiguration(configuration, active: true) + isActive = true + } + + private func deactivate() throws { + audioSession.lockForConfiguration() + defer { audioSession.unlockForConfiguration() } + try audioSession.setConfiguration(configuration, active: false) + isActive = false + } + + private func toggleSpeaker(_ isEnabled: Bool) throws { + guard isEnabled != isSpeakerOn else { + return + } - func configure( - _ configuration: RTCAudioSessionConfiguration = .default, - audioOn: Bool, - speakerOn: Bool - ) { - rtcAudioSession.lockForConfiguration() - defer { rtcAudioSession.unlockForConfiguration() } - rtcAudioSession.useManualAudio = true - rtcAudioSession.isAudioEnabled = audioOn + audioSession.lockForConfiguration() + defer { audioSession.unlockForConfiguration() } + + configuration.categoryOptions = isEnabled + ? defaultCategoryOptions.union(.defaultToSpeaker) + : defaultCategoryOptions + try audioSession.setConfiguration(configuration) + try audioSession.overrideOutputAudioPort(isEnabled ? .speaker : .none) + isSpeakerOn = isEnabled + + log.debug( + "Attempted to set speakerOn:\(isEnabled) with categoryOptions:\(configuration.categoryOptions). Current route: \(audioSession.currentRoute).", + subsystems: .webRTC + ) + } + + private func configureRouteChangeListener() { + routeChangeCancellable = NotificationCenter + .default + .publisher(for: AVAudioSession.routeChangeNotification) + .compactMap { notification -> AVAudioSession.RouteChangeReason? in + guard + let userInfo = notification.userInfo, + let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) + else { return nil } + return reason + } + .filter { + Set( + [ + .newDeviceAvailable, + .oldDeviceUnavailable, + .routeConfigurationChange, + .categoryChange, + .wakeFromSleep + ] + ).contains($0) + } + .log(.debug) { "AudioSession route updated due to \($0)." } + .sink { [weak self] _ in self?.updateAudioRoute() } + } + private func updateAudioRoute() { + let currentRoute = audioSession.currentRoute do { - log.debug( - """ - Configuring audio session - audioOn: \(audioOn) - speakerOn: \(speakerOn) - """ - ) - if speakerOn { - configuration.categoryOptions.insert(.defaultToSpeaker) - configuration.mode = AVAudioSession.Mode.videoChat.rawValue + if currentRoute.isExternal { + delegate?.audioSessionUpdated(self, speakerEnabled: false) } else { - configuration.categoryOptions.remove(.defaultToSpeaker) - configuration.mode = AVAudioSession.Mode.voiceChat.rawValue + try toggleSpeaker(activeCallSettings?.speakerOn ?? false) } - try rtcAudioSession.setConfiguration(configuration, active: audioOn) } catch { - log.error("Error occured while configuring audio session", error: error) + log.error("Failed to update route for \(currentRoute.description).", subsystems: .webRTC, error: error) } } - - func setAudioSessionEnabled(_ enabled: Bool) { - rtcAudioSession.lockForConfiguration() - defer { rtcAudioSession.unlockForConfiguration() } - rtcAudioSession.isAudioEnabled = enabled +} + +extension AVAudioSessionRouteDescription { + + private static let externalPorts: Set = [ + .bluetoothA2DP, .bluetoothLE, .bluetoothHFP, .carAudio, .headphones + ] + + var isExternal: Bool { + outputs.map(\.portType).contains { Self.externalPorts.contains($0) } } +} - deinit { - rtcAudioSession.lockForConfiguration() - rtcAudioSession.isAudioEnabled = false - rtcAudioSession.unlockForConfiguration() +extension AVAudioSession.RouteChangeReason: CustomStringConvertible { + public var description: String { + switch self { + case .unknown: + return ".unknown" + case .newDeviceAvailable: + return ".newDeviceAvailable" + case .oldDeviceUnavailable: + return ".oldDeviceUnavailable" + case .categoryChange: + return ".categoryChange" + case .override: + return ".override" + case .wakeFromSleep: + return ".wakeFromSleep" + case .noSuitableRouteForCategory: + return ".noSuitableRouteForCategory" + case .routeConfigurationChange: + return ".routeConfigurationChange" + @unknown default: + return "Unknown Reason" + } + } +} + +extension AVAudioSession.CategoryOptions: CustomStringConvertible { + public var description: String { + var options: [String] = [] + + if contains(.mixWithOthers) { + options.append(".mixWithOthers") + } + if contains(.duckOthers) { + options.append(".duckOthers") + } + if contains(.allowBluetooth) { + options.append(".allowBluetooth") + } + if contains(.defaultToSpeaker) { + options.append(".defaultToSpeaker") + } + if contains(.interruptSpokenAudioAndMixWithOthers) { + options.append(".interruptSpokenAudioAndMixWithOthers") + } + if contains(.allowBluetoothA2DP) { + options.append(".allowBluetoothA2DP") + } + if contains(.allowAirPlay) { + options.append(".allowAirPlay") + } + if #available(iOS 14.5, *) { + if contains(.overrideMutedMicrophoneInterruption) { + options.append(".overrideMutedMicrophoneInterruption") + } + } + + return options.isEmpty ? ".noOptions" : options.joined(separator: ", ") + } +} + +extension AVAudioSessionPortDescription { + override public var description: String { + "" } } extension RTCAudioSessionConfiguration { - + static let `default`: RTCAudioSessionConfiguration = { let configuration = RTCAudioSessionConfiguration.webRTC() var categoryOptions: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP] @@ -70,3 +238,8 @@ extension RTCAudioSessionConfiguration { return configuration }() } + +protocol AudioSessionDelegate: AnyObject { + + func audioSessionUpdated(_ audioSession: AudioSession, speakerEnabled: Bool) +} diff --git a/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/AudioMediaAdapter.swift b/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/AudioMediaAdapter.swift index 0daf9dc0d..00651c270 100644 --- a/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/AudioMediaAdapter.swift +++ b/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/AudioMediaAdapter.swift @@ -140,30 +140,6 @@ final class AudioMediaAdapter: MediaAdapting, @unchecked Sendable { try await localMediaManager.didUpdateCallSettings(settings) } - // MARK: - AudioSession - - /// Updates the audio session state. - /// - /// - Parameter isEnabled: Whether the audio session is enabled. - func didUpdateAudioSessionState(_ isEnabled: Bool) async { - await audioSession.setAudioSessionEnabled(isEnabled) - } - - /// Updates the audio session speaker state. - /// - /// - Parameters: - /// - isEnabled: Whether the speaker is enabled. - /// - audioSessionEnabled: Whether the audio session is enabled. - func didUpdateAudioSessionSpeakerState( - _ isEnabled: Bool, - with audioSessionEnabled: Bool - ) async { - await audioSession.configure( - audioOn: audioSessionEnabled, - speakerOn: isEnabled - ) - } - // MARK: - Observers /// Adds a new audio stream and notifies observers. diff --git a/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalAudioMediaAdapter.swift b/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalAudioMediaAdapter.swift index e7a221f40..f5028e99a 100644 --- a/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalAudioMediaAdapter.swift +++ b/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalAudioMediaAdapter.swift @@ -36,6 +36,8 @@ final class LocalAudioMediaAdapter: LocalMediaAdapting { /// The RTP transceiver for sending audio. private var sender: RTCRtpTransceiver? + private var lastUpdatedCallSettings: CallSettings.Audio? + /// The mid (Media Stream Identification) of the sender. var mid: String? { sender?.mid } @@ -192,63 +194,48 @@ final class LocalAudioMediaAdapter: LocalMediaAdapting { _ settings: CallSettings ) async throws { guard let localTrack else { return } - let isMuted = !settings.audioOn - // Check if the localTrack is muted - let isLocalMuted = localTrack.isEnabled == false - // Check if the speaker is on, for the current AudioSession - let isLocalSpeakerOn = await audioSession.isSpeakerOn - // Check if the current AudioSession isActive - let isLocalAudioSessionActive = await audioSession.isActive - - log.debug( - """ - Will try to update callSettings on \(type(of: self)) - isLocalMuted: \(isLocalMuted) - isLocalSpeakerOn: \(isLocalSpeakerOn) - isLocalAudioSessionActive: \(isLocalAudioSessionActive) - - \(settings) - """, - subsystems: .webRTC - ) - guard - sender == nil - || isMuted != isLocalMuted - || settings.speakerOn != isLocalSpeakerOn - || settings.audioOutputOn != isLocalAudioSessionActive - else { + guard lastUpdatedCallSettings != settings.audio else { return } - try await sfuAdapter.updateTrackMuteState( - .audio, - isMuted: isMuted, - for: sessionID - ) + let isMuted = !settings.audioOn + let isLocalMuted = localTrack.isEnabled == false + + if isMuted != isLocalMuted { + try await sfuAdapter.updateTrackMuteState( + .audio, + isMuted: isMuted, + for: sessionID + ) + } - await audioSession.configure( - audioOn: settings.audioOutputOn, - speakerOn: settings.speakerOn - ) + try audioSession.didUpdate(settings) if isMuted, localTrack.isEnabled == true { unpublish() } else if !isMuted { publish() await audioRecorder.startRecording() - let isActive = await audioSession.isActive - let isAudioEnabled = await audioSession.isAudioEnabled - log.debug( - """ - Local audioTrack is now published. - isEnabled: \(localTrack.isEnabled == true) - senderHasCorrectTrack: \(sender?.sender.track == localTrack) - trackId:\(localTrack.trackId) - audioSession.isActive: \(isActive) - audioSession.isAudioEnabled: \(isAudioEnabled) - """ - ) } + + lastUpdatedCallSettings = settings.audio + } +} + +extension CallSettings { + + struct Audio: Equatable { + var micOn: Bool + var speakerOn: Bool + var audioSessionOn: Bool + } + + var audio: Audio { + .init( + micOn: audioOn, + speakerOn: speakerOn, + audioSessionOn: audioOutputOn + ) } } diff --git a/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/MediaAdapter.swift b/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/MediaAdapter.swift index 08ea5bdaa..7cd0ee196 100644 --- a/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/MediaAdapter.swift +++ b/Sources/StreamVideo/WebRTC/v2/PeerConnection/MediaAdapters/MediaAdapter.swift @@ -223,30 +223,6 @@ final class MediaAdapter { } } - // MARK: - Audio - - /// Updates the audio session state. - /// - /// - Parameter isEnabled: Whether the audio session is enabled. - func didUpdateAudioSessionState(_ isEnabled: Bool) async { - await audioMediaAdapter.didUpdateAudioSessionState(isEnabled) - } - - /// Updates the audio session speaker state. - /// - /// - Parameters: - /// - isEnabled: Whether the speaker is enabled. - /// - audioSessionEnabled: Whether the audio session is enabled. - func didUpdateAudioSessionSpeakerState( - _ isEnabled: Bool, - with audioSessionEnabled: Bool - ) async { - await audioMediaAdapter.didUpdateAudioSessionSpeakerState( - isEnabled, - with: audioSessionEnabled - ) - } - // MARK: - Video /// Updates the camera position. diff --git a/Sources/StreamVideo/WebRTC/v2/PeerConnection/RTCPeerConnectionCoordinator.swift b/Sources/StreamVideo/WebRTC/v2/PeerConnection/RTCPeerConnectionCoordinator.swift index 3806e2d6a..6dceec409 100644 --- a/Sources/StreamVideo/WebRTC/v2/PeerConnection/RTCPeerConnectionCoordinator.swift +++ b/Sources/StreamVideo/WebRTC/v2/PeerConnection/RTCPeerConnectionCoordinator.swift @@ -445,52 +445,6 @@ class RTCPeerConnectionCoordinator: @unchecked Sendable { try await peerConnection.statistics() } - // MARK: - Audio - - /// Updates the audio session state. - /// - /// - Parameter isEnabled: Whether the audio session should be enabled or disabled. - func didUpdateAudioSessionState(_ isEnabled: Bool) async { - log.debug( - """ - PeerConnection will update audioSession state - Identifier: \(identifier) - type:\(peerType) - sessionID: \(sessionId) - sfu: \(sfuAdapter.hostname) - audioSession state: \(isEnabled) - """, - subsystems: subsystem - ) - await mediaAdapter.didUpdateAudioSessionState(isEnabled) - } - - /// Updates the audio session speaker state. - /// - /// - Parameters: - /// - isEnabled: Whether the speaker should be enabled or disabled. - /// - audioSessionEnabled: Whether the audio session is currently enabled. - func didUpdateAudioSessionSpeakerState( - _ isEnabled: Bool, - with audioSessionEnabled: Bool - ) async { - log.debug( - """ - PeerConnection will update audioSession speakerState - Identifier: \(identifier) - type:\(peerType) - sessionID: \(sessionId) - sfu: \(sfuAdapter.hostname) - audioSession speakerState: \(isEnabled) - """, - subsystems: subsystem - ) - await mediaAdapter.didUpdateAudioSessionSpeakerState( - isEnabled, - with: audioSessionEnabled - ) - } - // MARK: - Video /// Updates the camera position. diff --git a/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift b/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift index 345169499..de1e83007 100644 --- a/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift +++ b/Sources/StreamVideo/WebRTC/v2/StateMachine/Stages/WebRTCCoordinator+Joined.swift @@ -362,14 +362,7 @@ extension WebRTCCoordinator.StateMachine.Stage { .$callSettings .compactMap { $0 } .removeDuplicates() - .log(.debug, subsystems: .webRTC) { - """ - CallSettings updated - audioOn: \($0.audioOn) - videoOn: \($0.videoOn) - audioOutputOn: \($0.audioOutputOn) - """ - } + .log(.debug, subsystems: .webRTC) { "Updated \($0)" } .sinkTask(storeIn: disposableBag) { [weak self] callSettings in guard let self else { return } diff --git a/Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift b/Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift index ac81bcec7..a09567040 100644 --- a/Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift +++ b/Sources/StreamVideo/WebRTC/v2/WebRTCStateAdapter.swift @@ -10,7 +10,7 @@ import StreamWebRTC /// video call. This class manages the connection setup, track handling, and /// participants, including their media settings, capabilities, and track /// updates. -actor WebRTCStateAdapter: ObservableObject { +actor WebRTCStateAdapter: ObservableObject, AudioSessionDelegate { typealias ParticipantsStorage = [String: CallParticipant] typealias ParticipantOperation = @Sendable(ParticipantsStorage) -> ParticipantsStorage @@ -112,6 +112,8 @@ actor WebRTCStateAdapter: ObservableObject { self.rtcPeerConnectionCoordinatorFactory = rtcPeerConnectionCoordinatorFactory self.videoCaptureSessionProvider = videoCaptureSessionProvider self.screenShareSessionProvider = screenShareSessionProvider + + audioSession.delegate = self } /// Sets the session ID. @@ -541,4 +543,20 @@ actor WebRTCStateAdapter: ObservableObject { partialResult[entry.key] = newParticipant } } + + // MARK: - AudioSessionDelegate + + nonisolated func audioSessionUpdated( + _ audioSession: AudioSession, + speakerEnabled: Bool + ) { + Task { + await self.set( + callSettings: callSettings.withUpdatedSpeakerState( + speakerEnabled + ) + ) + log.debug("AudioSession updated with speakerEnabled:\(speakerEnabled).") + } + } } diff --git a/Sources/StreamVideoSwiftUI/Utils/ToastView.swift b/Sources/StreamVideoSwiftUI/Utils/ToastView.swift index 2479315d3..aecece7ac 100644 --- a/Sources/StreamVideoSwiftUI/Utils/ToastView.swift +++ b/Sources/StreamVideoSwiftUI/Utils/ToastView.swift @@ -59,8 +59,8 @@ public struct ToastView: View { public struct ToastModifier: ViewModifier { @Binding var toast: Toast? - @State private var workItem: DispatchWorkItem? - + @State private var workItem: Task? + public init(toast: Binding) { _toast = toast } @@ -107,13 +107,11 @@ public struct ToastModifier: ViewModifier { if toast.duration > 0 { workItem?.cancel() - - let task = DispatchWorkItem { + + workItem = Task { @MainActor in + try? await Task.sleep(nanoseconds: UInt64(toast.duration * Double(1_000_000_000))) dismissToast() } - - workItem = task - DispatchQueue.main.asyncAfter(deadline: .now() + toast.duration, execute: task) } } diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index e167b9448..05642a54b 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -311,6 +311,8 @@ 4097B3832BF4E37B0057992D /* OnChangeViewModifier_iOS13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4097B3822BF4E37B0057992D /* OnChangeViewModifier_iOS13.swift */; }; 40986C3A2CCB6D2F00510F88 /* RTCRtpEncodingParameters_Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40986C392CCB6D2F00510F88 /* RTCRtpEncodingParameters_Test.swift */; }; 40986C3C2CCB6E4B00510F88 /* RTCRtpTransceiverInit_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40986C3B2CCB6E4B00510F88 /* RTCRtpTransceiverInit_Tests.swift */; }; + 40986C3E2CD1148F00510F88 /* AudioSession_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40986C3D2CD1148F00510F88 /* AudioSession_Tests.swift */; }; + 40986C402CD12C5700510F88 /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40986C3F2CD12C5700510F88 /* AudioSession.swift */; }; 409CA7992BEE21720045F7AA /* XCTestCase+PredicateFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409CA7982BEE21720045F7AA /* XCTestCase+PredicateFulfillment.swift */; }; 40A0E9602B88ABC80089E8D3 /* DemoBackgroundEffectSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A0E95F2B88ABC80089E8D3 /* DemoBackgroundEffectSelector.swift */; }; 40A0E9622B88D3DC0089E8D3 /* UIInterfaceOrientation+CGOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A0E9612B88D3DC0089E8D3 /* UIInterfaceOrientation+CGOrientation.swift */; }; @@ -1187,7 +1189,6 @@ 84E86D4F2905E731004BA44C /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E86D4E2905E731004BA44C /* Utils.swift */; }; 84EA5D3C28BFB890004D3531 /* CallParticipantImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EA5D3B28BFB890004D3531 /* CallParticipantImageView.swift */; }; 84EA5D3F28C09AAC004D3531 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EA5D3E28C09AAB004D3531 /* CallController.swift */; }; - 84EA5D4328C1E944004D3531 /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EA5D4228C1E944004D3531 /* AudioSession.swift */; }; 84EBA4A22A72B81100577297 /* BroadcastBufferConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EBA4A12A72B81100577297 /* BroadcastBufferConnection.swift */; }; 84ED240D286C9515002A3186 /* DemoCallContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED240C286C9515002A3186 /* DemoCallContainerView.swift */; }; 84F07BD12CB4804900422E58 /* NoiseCancellationSettingsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F07BD02CB4804900422E58 /* NoiseCancellationSettingsRequest.swift */; }; @@ -1632,6 +1633,8 @@ 4097B3822BF4E37B0057992D /* OnChangeViewModifier_iOS13.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnChangeViewModifier_iOS13.swift; sourceTree = ""; }; 40986C392CCB6D2F00510F88 /* RTCRtpEncodingParameters_Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCRtpEncodingParameters_Test.swift; sourceTree = ""; }; 40986C3B2CCB6E4B00510F88 /* RTCRtpTransceiverInit_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCRtpTransceiverInit_Tests.swift; sourceTree = ""; }; + 40986C3D2CD1148F00510F88 /* AudioSession_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSession_Tests.swift; sourceTree = ""; }; + 40986C3F2CD12C5700510F88 /* AudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSession.swift; sourceTree = ""; }; 409CA7982BEE21720045F7AA /* XCTestCase+PredicateFulfillment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+PredicateFulfillment.swift"; sourceTree = ""; }; 40A0E95F2B88ABC80089E8D3 /* DemoBackgroundEffectSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoBackgroundEffectSelector.swift; sourceTree = ""; }; 40A0E9612B88D3DC0089E8D3 /* UIInterfaceOrientation+CGOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIInterfaceOrientation+CGOrientation.swift"; sourceTree = ""; }; @@ -2403,7 +2406,6 @@ 84E86D4E2905E731004BA44C /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 84EA5D3B28BFB890004D3531 /* CallParticipantImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipantImageView.swift; sourceTree = ""; }; 84EA5D3E28C09AAB004D3531 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; - 84EA5D4228C1E944004D3531 /* AudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSession.swift; sourceTree = ""; }; 84EBA4A12A72B81100577297 /* BroadcastBufferConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BroadcastBufferConnection.swift; sourceTree = ""; }; 84EBAA92288C137E00BE3176 /* Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modifiers.swift; sourceTree = ""; }; 84ED240C286C9515002A3186 /* DemoCallContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoCallContainerView.swift; sourceTree = ""; }; @@ -4954,6 +4956,7 @@ 8414081229F28B5600FF2D7C /* RTCConfiguration_Tests.swift */, 8446AF902A4D84F4002AB07B /* Retries_Tests.swift */, 841FF5042A5D815700809BBB /* VideoCapturerUtils_Tests.swift */, + 40986C3D2CD1148F00510F88 /* AudioSession_Tests.swift */, ); path = WebRTC; sourceTree = ""; @@ -5405,7 +5408,7 @@ 84FC2C2328AD1B5E00181490 /* WebRTCEventDecoder.swift */, 84FC2C2728AD350100181490 /* WebRTCEvents.swift */, 84BBF62A28AFC24000387A02 /* PeerConnectionFactory.swift */, - 84EA5D4228C1E944004D3531 /* AudioSession.swift */, + 40986C3F2CD12C5700510F88 /* AudioSession.swift */, 84BBF62C28AFC72700387A02 /* DefaultRTCMediaConstraints.swift */, 8411925D28C5E5D00074EF88 /* DefaultRTCConfiguration.swift */, ); @@ -6301,7 +6304,6 @@ 4012B1962BFCAC26006B0031 /* StreamCallStateMachine+RejectedStage.swift in Sources */, 846E4AEF29CDEA66003733AB /* WSAuthMessageRequest.swift in Sources */, 406583992B877AB400B4F979 /* CIImage+Resize.swift in Sources */, - 84EA5D4328C1E944004D3531 /* AudioSession.swift in Sources */, 40BBC4A42C623D03002AEF92 /* RTCRtpTransceiverInit+Convenience.swift in Sources */, 841BAA382BD15CDE000C73E4 /* CallTimeline.swift in Sources */, 8449824D2C738A830029734D /* StartRTMPBroadcastsResponse.swift in Sources */, @@ -6606,6 +6608,7 @@ 841FF51B2A5FED4800809BBB /* SystemEnvironment+XStreamClient.swift in Sources */, 84DC38A329ADFCFD00946713 /* MuteUsersResponse.swift in Sources */, 84DC38BF29ADFCFD00946713 /* ScreensharingSettings.swift in Sources */, + 40986C402CD12C5700510F88 /* AudioSession.swift in Sources */, 843DAB9929E695CF00E0EB63 /* CreateGuestResponse.swift in Sources */, 84DC389229ADFCFD00946713 /* RequestPermissionRequest.swift in Sources */, 84C28C922A84D16A00742E33 /* GoLiveRequest.swift in Sources */, @@ -6698,6 +6701,7 @@ 40F017392BBEAF6400E89FD1 /* MockCallKitService.swift in Sources */, 403FB1602BFE22840047A696 /* StreamCallStateMachineStageRejectingStage_Tests.swift in Sources */, 40F017402BBEBC6500E89FD1 /* MockCallKitPushNotificationAdapter.swift in Sources */, + 40986C3E2CD1148F00510F88 /* AudioSession_Tests.swift in Sources */, 403FB1512BFE1AA90047A696 /* StreamCallStateMachine_Tests.swift in Sources */, 406B3C532C92007900FC93A1 /* WebRTCCoordinatorStateMachine_ConnectedStageTests.swift in Sources */, 84F58B8129EE9C4900010C4C /* WebSocketPingController_Delegate.swift in Sources */, diff --git a/StreamVideoTests/WebRTC/AudioSession_Tests.swift b/StreamVideoTests/WebRTC/AudioSession_Tests.swift new file mode 100644 index 000000000..9abcded64 --- /dev/null +++ b/StreamVideoTests/WebRTC/AudioSession_Tests.swift @@ -0,0 +1,61 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +@testable import StreamVideo +import StreamWebRTC +import XCTest + +final class AudioSession_Tests: XCTestCase { + + private lazy var subject: AudioSession! = .init() + + // MARK: - Lifecycle + + override func tearDown() { + subject = nil + super.tearDown() + } + + // MARK: - configure + + func test_configure_givenAudioOnAndSpeakerOn_whenConfiguring_thenAudioAndSpeakerAreEnabled() async { + // When + await subject.configure(audioOn: true, speakerOn: true) + + // Then + XCTAssertTrue(RTCAudioSession.sharedInstance().isActive) + XCTAssertTrue(AVAudioSession.sharedInstance().categoryOptions.contains(.defaultToSpeaker)) + XCTAssertEqual(AVAudioSession.sharedInstance().mode, .videoChat) + } + + func test_configure_givenAudioOffAndSpeakerOff_whenConfiguring_thenAudioIsOffAndSpeakerNotEnabled() async { + // When + await subject.configure(audioOn: false, speakerOn: false) + + // Then + XCTAssertFalse(RTCAudioSession.sharedInstance().isActive) + XCTAssertFalse(AVAudioSession.sharedInstance().categoryOptions.contains(.defaultToSpeaker)) + XCTAssertEqual(AVAudioSession.sharedInstance().mode, .voiceChat) + } + + func test_setAudioSessionEnabled_givenEnabledTrue_whenSettingAudioEnabled_thenAudioSessionIsEnabled() async { + // When + await subject.setAudioSessionEnabled(true) + + // Then + XCTAssertTrue(RTCAudioSession.sharedInstance().isAudioEnabled) + } + + // MARK: - deinit + + func test_deinit_givenAudioSession_whenDeinitialized_thenAudioSessionIsDisabled() async { + _ = subject + + await subject.setAudioSessionEnabled(true) + subject = nil + + // Then + XCTAssertFalse(RTCAudioSession.sharedInstance().isAudioEnabled) + } +} diff --git a/StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/AudioMediaAdapter_Tests.swift b/StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/AudioMediaAdapter_Tests.swift index c2cd6e886..a3db2f249 100644 --- a/StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/AudioMediaAdapter_Tests.swift +++ b/StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/AudioMediaAdapter_Tests.swift @@ -61,24 +61,4 @@ final class AudioMediaAdapter_Tests: XCTestCase { ) XCTAssertEqual(actual, settings) } - - // MARK: - didUpdateAudioSessionState(_:) - - func test_didUpdateAudioSessionState_audioSessionWasConfiguredCorrectly() async throws { - await subject.didUpdateAudioSessionState(true) - - let isActive = await audioSession.isAudioEnabled - XCTAssertTrue(isActive) - } - - // MARK: - didUpdateAudioSessionSpeakerState(_:) - - func test_didUpdateAudioSessionSpeakerState_audioSessionWasConfiguredCorrectly() async throws { - await subject.didUpdateAudioSessionSpeakerState(true, with: false) - - let isActive = await audioSession.isActive - let isSpeakerOn = await audioSession.isSpeakerOn - XCTAssertFalse(isActive) - XCTAssertTrue(isSpeakerOn) - } } diff --git a/StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalAudioMediaAdapter_Tests.swift b/StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalAudioMediaAdapter_Tests.swift index 23b1a5047..5aefb364a 100644 --- a/StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalAudioMediaAdapter_Tests.swift +++ b/StreamVideoTests/WebRTC/v2/PeerConnection/MediaAdapters/LocalMediaAdapters/LocalAudioMediaAdapter_Tests.swift @@ -172,7 +172,7 @@ final class LocalAudioMediaAdapter_Tests: XCTestCase { ) subject.localTrack?.isEnabled = true - try await subject.didUpdateCallSettings(.init(audioOn: false)) + try await subject.didUpdateCallSettings(.init(audioOutputOn: false)) let isActive = await audioSession.isActive XCTAssertFalse(isActive)