Skip to content

Commit 195ccb9

Browse files
authored
Merge pull request #292 from shiguredo/feature/audio-hard-mute
音声ソフトミュート/ハードミュート機能追加
2 parents 887f5e1 + 128f2b1 commit 195ccb9

File tree

7 files changed

+133
-5
lines changed

7 files changed

+133
-5
lines changed

CHANGES.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,27 @@
1111

1212
## develop
1313

14-
- [UPDATE] libwebrtc m144.7559.0.1 に上げる
15-
- @miosakuma @zztkm
14+
- [UPDATE] libwebrtc m144.7559.2.1 に上げる
15+
- @t-miya
1616
- [UPDATE] Statistics, StatisticsEntry をドキュメント対象として公開する
1717
- `getStats` メソッドの返り値である `Statistics` のドキュメントを生成するため
1818
- @t-miya
1919
- [UPDATE] Configuration.simulcastRid を非推奨にする
2020
- 移行先は `Configuration.simulcastRequestRid`
2121
- @zztkm
22+
- [ADD] MediaChannel に `setAudioSoftMute(_:)` を追加する
23+
- 音声ソフトミュート機能のシンタックスシュガー
24+
- 送信ストリームの AudioTrack を取得し、MediaStream.audioEnabled を切り替える
25+
- AudioTrack の有無判定を行うため、 MediaStream に `hasAudioTrack` を追加する
26+
- @t-miya
27+
- [ADD] 音声のハードミュート有効化/無効化機能を追加する
28+
- iOS 端末のマイクインジケーターを消灯させる
29+
- AudioDeviceModuleWrapper クラスを追加する
30+
- RTCAudioDeviceModule の pauseRecording/resumeRecording を実行するためのラッパークラス
31+
- インスタンスは NativePeerChannelFactory が保持する
32+
- MediaChannel に setAudioHardMute(_:) を追加する
33+
- 内部で NativePeerChannelFactory 経由で AudioDeviceModuleWrapper.setAudioHardMute(_:) を呼び出す
34+
- @t-miya
2235
- [ADD] MediaChannel に libwebrtc の統計情報を取得する `getStats` メソッドを追加する
2336
- @t-miya
2437
- [ADD] RTCAudioTrack から音声データを受け取るためのコールバックプロトコルである RTCAudioTrackSink を追加する

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import Foundation
44
import PackageDescription
55

6-
let libwebrtcVersion = "m144.7559.0.1"
6+
let libwebrtcVersion = "m144.7559.2.1"
77

88
let package = Package(
99
name: "Sora",
@@ -21,7 +21,7 @@ let package = Package(
2121
.binaryTarget(
2222
name: "WebRTC",
2323
url: "https://github.com/shiguredo-webrtc-build/webrtc-build/releases/download/\(libwebrtcVersion)/WebRTC.xcframework.zip",
24-
checksum: "7429661f35274871f28b74c8b7105c6cc9c899c98fc5f6b08e59ec0aece8e529"
24+
checksum: "610c342f3e8f6199e2b6672fd3e6102876cbd2a131470ef0077de7b3a895d011"
2525
),
2626
.target(
2727
name: "Sora",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Foundation
2+
import WebRTC
3+
4+
/// RTCAudioDeviceModule の録音ポーズ/再開をラップするクラス
5+
internal final class AudioDeviceModuleWrapper {
6+
private let audioDeviceModule: RTCAudioDeviceModule
7+
// ハードミュート処理を直列化するためのキュー
8+
private let queue = DispatchQueue(label: "jp.shiguredo.sora.audio.device.wrapper")
9+
// 現在のハードミュート状態
10+
private var isHardMuted: Bool = false
11+
12+
init(audioDeviceModule: RTCAudioDeviceModule) {
13+
self.audioDeviceModule = audioDeviceModule
14+
}
15+
16+
/// 音声のハードミュート有効化/無効化します
17+
/// - Parameter mute: `true` でミュート有効化、`false` でミュート無効化
18+
/// - Returns: 成功した場合は `true`、失敗した場合は `false` を返します
19+
func setAudioHardMute(_ mute: Bool) -> Bool {
20+
queue.sync {
21+
guard isHardMuted != mute else {
22+
return true
23+
}
24+
25+
let internalResult = mute ? pauseRecordingInternal() : resumeRecordingInternal()
26+
let message = "setAudioHardMute via RTCAudioDeviceModule mute=\(mute)"
27+
let result = internalResult == 0
28+
if result {
29+
isHardMuted = mute
30+
Logger.debug(type: .mediaChannel, message: message)
31+
return true
32+
} else {
33+
Logger.error(type: .mediaChannel, message: "\(message) failed")
34+
return false
35+
}
36+
}
37+
}
38+
39+
private func pauseRecordingInternal() -> Int32 {
40+
Int32(audioDeviceModule.pauseRecording())
41+
}
42+
43+
private func resumeRecordingInternal() -> Int32 {
44+
Int32(audioDeviceModule.resumeRecording())
45+
}
46+
}

Sora/MediaChannel.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,53 @@ public final class MediaChannel {
521521
return result
522522
? nil : SoraError.messagingError(reason: "failed to send message: label => \(label)")
523523
}
524+
525+
/// MediaChannel の接続中にマイクをハードミュート有効化/無効化します
526+
/// - Parameter mute: `true` で有効化、`false` で無効化
527+
/// - Returns: 成功した場合は `nil`、失敗した場合は `Error` を返します
528+
public func setAudioHardMute(_ mute: Bool) -> Error? {
529+
guard state == .connected else {
530+
return SoraError.mediaChannelError(
531+
reason: "MediaChannel is not connected (state: \(state))")
532+
}
533+
534+
guard configuration.audioEnabled else {
535+
return SoraError.mediaChannelError(reason: "audioEnabled is false")
536+
}
537+
538+
if !NativePeerChannelFactory.default.audioDeviceModuleWrapper.setAudioHardMute(mute) {
539+
return SoraError.mediaChannelError(
540+
reason: "AudioDeviceModuleWrapper::setAudioHardMute failed")
541+
}
542+
543+
return nil
544+
}
545+
546+
/// MediaChannel の接続中にマイクをソフトミュート有効化 / 無効化します
547+
/// - Parameter mute: `true` で有効化、`false` で無効化
548+
/// - Returns: 成功した場合は `nil`、失敗した場合は `Error` を返します
549+
public func setAudioSoftMute(_ mute: Bool) -> Error? {
550+
guard state == .connected else {
551+
return SoraError.mediaChannelError(
552+
reason: "MediaChannel is not connected (state: \(state))")
553+
}
554+
555+
guard configuration.audioEnabled else {
556+
return SoraError.mediaChannelError(reason: "audioEnabled is false")
557+
}
558+
559+
guard let senderStream else {
560+
return SoraError.mediaChannelError(reason: "senderStream is unavailable")
561+
}
562+
563+
guard senderStream.hasAudioTrack else {
564+
return SoraError.mediaChannelError(reason: "senderStream has no AudioTrack")
565+
}
566+
567+
senderStream.audioEnabled = !mute
568+
Logger.debug(type: .mediaChannel, message: "setAudioSoftMute mute=\(mute)")
569+
return nil
570+
}
524571
}
525572

526573
extension MediaChannel: CustomStringConvertible {

Sora/MediaStream.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public protocol MediaStream: AnyObject {
5959
/// サーバーへの送受信を停止しても、マイクはミュートされませんので注意してください。
6060
var audioEnabled: Bool { get set }
6161

62+
/// 音声トラックを保持している場合は ``true`` を返します。
63+
///
64+
/// SDK 内部で利用する判定用のプロパティです。
65+
var hasAudioTrack: Bool { get }
66+
6267
/// 受信した音声のボリューム。 0 から 10 (含む) までの値をセットします。
6368
/// このプロパティはロールがサブスクライバーの場合のみ有効です。
6469
var remoteAudioVolume: Double? { get set }
@@ -197,6 +202,10 @@ class BasicMediaStream: MediaStream {
197202
}
198203
}
199204

205+
var hasAudioTrack: Bool {
206+
nativeAudioTrack != nil
207+
}
208+
200209
var remoteAudioVolume: Double? {
201210
get {
202211
nativeAudioTrack?.source.volume

Sora/NativePeerChannelFactory.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,26 @@ class WrapperVideoEncoderFactory: NSObject, RTCVideoEncoderFactory {
3333
class NativePeerChannelFactory {
3434
static var `default` = NativePeerChannelFactory()
3535

36+
let audioDeviceModule: RTCAudioDeviceModule
37+
/// 録音ポーズ/再開制御用に保持する ADM ラッパー
38+
let audioDeviceModuleWrapper: AudioDeviceModuleWrapper
39+
3640
var nativeFactory: RTCPeerConnectionFactory
3741

3842
init() {
3943
Logger.debug(type: .peerChannel, message: "create native peer channel factory")
4044

45+
audioDeviceModule = RTCAudioDeviceModule()
46+
audioDeviceModuleWrapper = AudioDeviceModuleWrapper(audioDeviceModule: audioDeviceModule)
47+
4148
// 映像コーデックのエンコーダーとデコーダーを用意する
4249
let encoder = WrapperVideoEncoderFactory.shared
4350
let decoder = RTCDefaultVideoDecoderFactory()
4451
nativeFactory =
4552
RTCPeerConnectionFactory(
4653
encoderFactory: encoder,
47-
decoderFactory: decoder)
54+
decoderFactory: decoder,
55+
audioDeviceModule: audioDeviceModule)
4856

4957
for info in encoder.supportedCodecs() {
5058
Logger.debug(

Sora/SoraError.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public enum SoraError: Error {
3333
/// ``PeerChannel`` で発生したエラー
3434
case peerChannelError(reason: String)
3535

36+
/// ``MediaChannel`` で発生したエラー
37+
case mediaChannelError(reason: String)
38+
3639
/// カメラに関するエラー
3740
case cameraError(reason: String)
3841

@@ -74,6 +77,8 @@ extension SoraError: LocalizedError {
7477
return "Unknown signaling message type \(type)"
7578
case .peerChannelError(let reason):
7679
return "PeerChannel error (\(reason))"
80+
case .mediaChannelError(let reason):
81+
return "MediaChannel error (\(reason))"
7782
case .cameraError(let reason):
7883
return "Camera error: \(reason)"
7984
case .messagingError(let reason):

0 commit comments

Comments
 (0)