From 298c68d5cb53c8309fd0a84b4c98475f555790d2 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 8 Feb 2025 22:08:11 +0900 Subject: [PATCH 1/5] mute mode --- Sources/LiveKit/Track/AudioManager.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/LiveKit/Track/AudioManager.swift b/Sources/LiveKit/Track/AudioManager.swift index 9350474de..882bce73d 100644 --- a/Sources/LiveKit/Track/AudioManager.swift +++ b/Sources/LiveKit/Track/AudioManager.swift @@ -258,6 +258,11 @@ public class AudioManager: Loggable { _state.mutate { $0.engineObservers = engineObservers } } + public var isLegacyMuteMode: Bool { + get { RTC.audioDeviceModule.muteMode == .restartEngine } + set { RTC.audioDeviceModule.muteMode = newValue ? .restartEngine : .voiceProcessing } + } + // MARK: - For testing var isPlayoutInitialized: Bool { From 75b8520a8b51fa3c4d3742b6eccb8c9f52721da4 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 8 Feb 2025 22:18:41 +0900 Subject: [PATCH 2/5] doc --- Sources/LiveKit/Track/AudioManager.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/LiveKit/Track/AudioManager.swift b/Sources/LiveKit/Track/AudioManager.swift index 882bce73d..98a8fee06 100644 --- a/Sources/LiveKit/Track/AudioManager.swift +++ b/Sources/LiveKit/Track/AudioManager.swift @@ -258,6 +258,12 @@ public class AudioManager: Loggable { _state.mutate { $0.engineObservers = engineObservers } } + /// Set to `true` to enable legacy mic mute mode. + /// + /// - Default: Uses `AVAudioEngine`'s `isVoiceProcessingInputMuted` internally. + /// This is fast, and muted speaker detection works. However, iOS will play a sound effect. + /// - Legacy: Restarts the internal `AVAudioEngine` without mic input when muted. + /// This is slower, and muted speaker detection does not work. No sound effect is played. public var isLegacyMuteMode: Bool { get { RTC.audioDeviceModule.muteMode == .restartEngine } set { RTC.audioDeviceModule.muteMode = newValue ? .restartEngine : .voiceProcessing } From 7b4dab5d980e63391aa5073e9fe099ebcf6917fa Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:30:47 +0900 Subject: [PATCH 3/5] lib 125.6422.18 --- Package.swift | 2 +- Package@swift-5.9.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index ce0471c29..f6779a701 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( ], dependencies: [ // LK-Prefixed Dynamic WebRTC XCFramework - .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.17"), + .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.18"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.26.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), // Only used for DocC generation diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 99d18d31d..b67016aa5 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -20,7 +20,7 @@ let package = Package( ], dependencies: [ // LK-Prefixed Dynamic WebRTC XCFramework - .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.17"), + .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.18"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.26.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), // Only used for DocC generation From 10ef6ad5a634542e8db4d3317ddaf9bf21f00b14 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 10 Feb 2025 21:34:03 +0900 Subject: [PATCH 4/5] tests --- Sources/LiveKit/Track/AudioManager.swift | 14 ++ Tests/LiveKitTests/MuteTests.swift | 174 ++++++++++++++++++ .../LiveKitTests/Support/AudioRecorder.swift | 2 +- 3 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 Tests/LiveKitTests/MuteTests.swift diff --git a/Sources/LiveKit/Track/AudioManager.swift b/Sources/LiveKit/Track/AudioManager.swift index 98a8fee06..6c97aa987 100644 --- a/Sources/LiveKit/Track/AudioManager.swift +++ b/Sources/LiveKit/Track/AudioManager.swift @@ -271,6 +271,20 @@ public class AudioManager: Loggable { // MARK: - For testing + var isEngineRunning: Bool { + RTC.audioDeviceModule.isEngineRunning + } + + var isMicrophoneMuted: Bool { + get { RTC.audioDeviceModule.isMicrophoneMuted } + set { RTC.audioDeviceModule.isMicrophoneMuted = newValue } + } + + var engineState: RTCAudioEngineState { + get { RTC.audioDeviceModule.engineState } + set { RTC.audioDeviceModule.engineState = newValue } + } + var isPlayoutInitialized: Bool { RTC.audioDeviceModule.isPlayoutInitialized } diff --git a/Tests/LiveKitTests/MuteTests.swift b/Tests/LiveKitTests/MuteTests.swift new file mode 100644 index 000000000..9fcb6ca0f --- /dev/null +++ b/Tests/LiveKitTests/MuteTests.swift @@ -0,0 +1,174 @@ +/* + * Copyright 2025 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@testable import LiveKit +import LiveKitWebRTC +import XCTest + +struct TestEngineTransition { + let outputEnabled: ValueOrAbsent + let outputRunning: ValueOrAbsent + let inputEnabled: ValueOrAbsent + let inputRunning: ValueOrAbsent + let legacyMuteMode: ValueOrAbsent + let inputMuted: ValueOrAbsent + + init(outputEnabled: ValueOrAbsent = .absent, + outputRunning: ValueOrAbsent = .absent, + inputEnabled: ValueOrAbsent = .absent, + inputRunning: ValueOrAbsent = .absent, + legacyMuteMode: ValueOrAbsent = .absent, + inputMuted: ValueOrAbsent = .absent) + { + self.outputEnabled = outputEnabled + self.outputRunning = outputRunning + self.inputEnabled = inputEnabled + self.inputRunning = inputRunning + self.legacyMuteMode = legacyMuteMode + self.inputMuted = inputMuted + } +} + +struct TestEngineAssert: Hashable { + let engineRunning: Bool +} + +struct TestEngineStep { + let transition: TestEngineTransition + let assert: TestEngineAssert +} + +extension RTCAudioEngineState: @retroactive CustomStringConvertible { + public var description: String { + "EngineState(" + + "outputEnabled: \(outputEnabled), " + + "outputRunning: \(outputRunning), " + + "inputEnabled: \(inputEnabled), " + + "inputRunning: \(inputRunning), " + + "inputMuted: \(inputMuted), " + + "muteMode: \(muteMode)" + + ")" + } +} + +func applyEngineTransition(_ transition: TestEngineTransition) { + let adm = AudioManager.shared + var engineState = adm.engineState + + if case let .value(value) = transition.outputEnabled { + engineState.outputEnabled = value + } + + if case let .value(value) = transition.outputRunning { + engineState.outputRunning = value + } + + if case let .value(value) = transition.inputEnabled { + engineState.inputEnabled = value + } + + if case let .value(value) = transition.inputRunning { + engineState.inputRunning = value + } + + if case let .value(value) = transition.inputMuted { + engineState.inputMuted = value + } + + if case let .value(value) = transition.legacyMuteMode { + engineState.muteMode = value ? .restartEngine : .voiceProcessing + } + + print("Testing engine state: \(engineState)") + adm.engineState = engineState +} + +let standardEngineSteps: [TestEngineStep] = [ + // Enable output + TestEngineStep(transition: .init(outputEnabled: .value(true)), assert: .init(engineRunning: false)), + TestEngineStep(transition: .init(outputRunning: .value(true)), assert: .init(engineRunning: true)), + // Enable input + TestEngineStep(transition: .init(inputEnabled: .value(true)), assert: .init(engineRunning: true)), + TestEngineStep(transition: .init(inputRunning: .value(true)), assert: .init(engineRunning: true)), + // Disable input + TestEngineStep(transition: .init(inputRunning: .value(false)), assert: .init(engineRunning: true)), + TestEngineStep(transition: .init(inputEnabled: .value(false)), assert: .init(engineRunning: true)), + // Disable output + TestEngineStep(transition: .init(outputRunning: .value(false)), assert: .init(engineRunning: false)), + TestEngineStep(transition: .init(outputEnabled: .value(false)), assert: .init(engineRunning: false)), +] + +let muteEngineSteps: [TestEngineStep] = [ + // Enable output + TestEngineStep(transition: .init(outputEnabled: .value(true)), assert: .init(engineRunning: false)), + TestEngineStep(transition: .init(outputRunning: .value(true)), assert: .init(engineRunning: true)), + + // Enable input + TestEngineStep(transition: .init(inputEnabled: .value(true)), assert: .init(engineRunning: true)), + TestEngineStep(transition: .init(inputRunning: .value(true)), assert: .init(engineRunning: true)), + + // Toggle mute + TestEngineStep(transition: .init(inputMuted: .value(true)), assert: .init(engineRunning: true)), + TestEngineStep(transition: .init(inputMuted: .value(false)), assert: .init(engineRunning: true)), + + // Enable legacy mute mode + TestEngineStep(transition: .init(legacyMuteMode: .value(true)), assert: .init(engineRunning: true)), + + // Disable output + TestEngineStep(transition: .init(outputRunning: .value(false)), assert: .init(engineRunning: true)), + TestEngineStep(transition: .init(outputEnabled: .value(false)), assert: .init(engineRunning: true)), + + // Engine should shut down at this point + TestEngineStep(transition: .init(inputMuted: .value(true)), assert: .init(engineRunning: false)), + + // Engine starts + TestEngineStep(transition: .init(inputMuted: .value(false)), assert: .init(engineRunning: true)), + + // Enable output + TestEngineStep(transition: .init(outputEnabled: .value(true)), assert: .init(engineRunning: true)), + TestEngineStep(transition: .init(outputRunning: .value(true)), assert: .init(engineRunning: true)), + + // Mute + TestEngineStep(transition: .init(inputMuted: .value(true)), assert: .init(engineRunning: true)), + + // Disable input + TestEngineStep(transition: .init(inputRunning: .value(false)), assert: .init(engineRunning: true)), + TestEngineStep(transition: .init(inputEnabled: .value(false)), assert: .init(engineRunning: true)), + + // Disable output + TestEngineStep(transition: .init(outputRunning: .value(false)), assert: .init(engineRunning: false)), + TestEngineStep(transition: .init(outputEnabled: .value(false)), assert: .init(engineRunning: false)), +] + +class MuteTests: XCTestCase { + override func setUp() { + super.setUp() + continueAfterFailure = false + } + + func testTransitions() async throws { + let adm = AudioManager.shared + + for step in muteEngineSteps { + applyEngineTransition(step.transition) + // Check if engine running state is correct. + XCTAssert(adm.isEngineRunning == step.assert.engineRunning) + + let ns = UInt64(1 * 1_000_000_000) + try await Task.sleep(nanoseconds: ns) + } + } +} diff --git a/Tests/LiveKitTests/Support/AudioRecorder.swift b/Tests/LiveKitTests/Support/AudioRecorder.swift index dc0cffe99..bd96980ea 100644 --- a/Tests/LiveKitTests/Support/AudioRecorder.swift +++ b/Tests/LiveKitTests/Support/AudioRecorder.swift @@ -23,7 +23,7 @@ class AudioRecorder { public let filePath: URL private var audioFile: AVAudioFile? - init(sampleRate: Double = 16000, channels: Int = 1) throws { + init(sampleRate: Double = 48000, channels: Int = 1) throws { self.sampleRate = sampleRate let settings: [String: Any] = [ From cfe4247479f9f6c6768941b2c4e6281800d6e79d Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 11 Feb 2025 00:40:43 +0900 Subject: [PATCH 5/5] Fix test --- Tests/LiveKitTests/MuteTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/LiveKitTests/MuteTests.swift b/Tests/LiveKitTests/MuteTests.swift index 9fcb6ca0f..5ecd2393c 100644 --- a/Tests/LiveKitTests/MuteTests.swift +++ b/Tests/LiveKitTests/MuteTests.swift @@ -51,7 +51,7 @@ struct TestEngineStep { let assert: TestEngineAssert } -extension RTCAudioEngineState: @retroactive CustomStringConvertible { +extension RTCAudioEngineState: CustomStringConvertible { public var description: String { "EngineState(" + "outputEnabled: \(outputEnabled), " +