Skip to content

Commit

Permalink
[Improvement]SpeakerManager callSettings observation (#589)
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis authored Nov 4, 2024
1 parent b06ec36 commit 0f23f47
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 20 deletions.
3 changes: 2 additions & 1 deletion Sources/StreamVideo/Call.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
}
}

self.callController.call = self
callController.call = self
speaker.call = self
// It's important to instantiate the stateMachine as soon as possible
// to ensure it's uniqueness.
_ = stateMachine
Expand Down
37 changes: 34 additions & 3 deletions Sources/StreamVideo/CallSettings/SpeakerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@ import Foundation

/// Handles the speaker state during a call.
public final class SpeakerManager: ObservableObject, CallSettingsManager, @unchecked Sendable {

internal let callController: CallController

@Published public internal(set) var status: CallSettingsStatus
@Published public internal(set) var audioOutputStatus: CallSettingsStatus

weak var call: Call? {
didSet { Task { await didUpdateCall(call) } }
}

internal let callController: CallController
internal let state = CallSettingsState()


private let disposableBag = DisposableBag()

init(
callController: CallController,
initialSpeakerStatus: CallSettingsStatus,
Expand Down Expand Up @@ -75,4 +82,28 @@ public final class SpeakerManager: ObservableObject, CallSettingsManager, @unche
}
)
}

@MainActor
private func didUpdateCall(_ call: Call?) {
let observationKey = "call-settings-cancellable"
disposableBag.remove(observationKey)

guard let call else {
return
}

let typeOfSelf = type(of: self)
call
.state
.$callSettings
.removeDuplicates()
.map { (speakerOn: $0.speakerOn, audioOutputOn: $0.audioOutputOn) }
.log(.debug) { "\(typeOfSelf) callSettings updated speakerOn:\($0.speakerOn) audioOutputOn:\($0.audioOutputOn)." }
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.status = $0.speakerOn ? .enabled : .disabled
self?.audioOutputStatus = $0.audioOutputOn ? .enabled : .disabled
}
.store(in: disposableBag, key: observationKey)
}
}
5 changes: 4 additions & 1 deletion Sources/StreamVideo/Utils/DisposableBag/DisposableBag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ public final class DisposableBag: @unchecked Sendable {
}

extension AnyCancellable {
public func store(in disposableBag: DisposableBag?) { disposableBag?.insert(self) }
public func store(
in disposableBag: DisposableBag?,
key: String = UUID().uuidString
) { disposableBag?.insert(self, with: key) }
}

extension Task {
Expand Down
72 changes: 57 additions & 15 deletions StreamVideoTests/CallSettings/SpeakerManager_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
//

@testable import StreamVideo
import XCTest
@preconcurrency import XCTest

final class SpeakerManager_Tests: XCTestCase {
final class SpeakerManager_Tests: XCTestCase, @unchecked Sendable {

// MARK: - disable

func test_speaker_disable() async throws {
// Given
Expand All @@ -14,56 +16,96 @@ final class SpeakerManager_Tests: XCTestCase {
initialSpeakerStatus: .enabled,
initialAudioOutputStatus: .enabled
)

// When
try await speakerManager.disableSpeakerPhone()

// Then
XCTAssert(speakerManager.status == .disabled)
}


// MARK: - enable

func test_speaker_enable() async throws {
// Given
let speakerManager = SpeakerManager(
callController: CallController_Mock.make(),
initialSpeakerStatus: .disabled,
initialAudioOutputStatus: .enabled
)

// When
try await speakerManager.enableSpeakerPhone()

// Then
XCTAssert(speakerManager.status == .enabled)
}

func test_speaker_disableSound() async throws {

// MARK: - disableAudioOutput

func test_speaker_disableAudioOutput() async throws {
// Given
let speakerManager = SpeakerManager(
callController: CallController_Mock.make(),
initialSpeakerStatus: .enabled,
initialAudioOutputStatus: .enabled
)

// When
try await speakerManager.disableAudioOutput()

// Then
XCTAssert(speakerManager.audioOutputStatus == .disabled)
}

func test_speaker_enableSound() async throws {

// MARK: - enableAudioOutput

func test_speaker_enableAudioOutput() async throws {
// Given
let speakerManager = SpeakerManager(
callController: CallController_Mock.make(),
initialSpeakerStatus: .enabled,
initialAudioOutputStatus: .disabled
)

// When
try await speakerManager.enableAudioOutput()

// Then
XCTAssert(speakerManager.audioOutputStatus == .enabled)
}

// MARK: - didUpdate callSettings

@MainActor
func test_didUpdateCall_updatesStatus() async throws {
// Given
let streamVideo = MockStreamVideo()
_ = streamVideo
let call = Call.dummy()

await wait(for: 0.5)
call.state.update(callSettings: .init(speakerOn: false, audioOutputOn: false))

await fulfillment {
call.speaker.status == .disabled
&& call.speaker.audioOutputStatus == .disabled
}
}

@MainActor
func test_toggleSpeaker_afterDidUpdateCall_updatesCorrectly() async throws {
// Given
let streamVideo = MockStreamVideo()
_ = streamVideo
let call = Call.dummy()
await wait(for: 0.5)
await fulfillment { call.speaker.status == .enabled && call.speaker.status == .enabled }
call.state.update(callSettings: .init(speakerOn: false, audioOutputOn: false))
await fulfillment { call.speaker.status == .disabled && call.speaker.status == .disabled }

try await call.speaker.toggleSpeakerPhone()

await fulfillment { call.speaker.status == .enabled }
}
}

0 comments on commit 0f23f47

Please sign in to comment.