Skip to content

Commit a202c09

Browse files
committed
setVideoSoftMute / setVideoHardMute を追加
1 parent 18332d3 commit a202c09

File tree

4 files changed

+182
-0
lines changed

4 files changed

+182
-0
lines changed

CHANGES.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@
2424
- 送信ストリームの AudioTrack を取得し、MediaStream.audioEnabled を切り替える
2525
- AudioTrack の有無判定を行うため、 MediaStream に `hasAudioTrack` を追加する
2626
- @t-miya
27+
- [ADD] MediaChannel に `setVideoSoftMute(_:)` を追加する
28+
- 映像ソフトミュート機能のシンタックスシュガー
29+
- 送信ストリームの VideoTrack を取得し、MediaStream.videoEnabled を切り替える
30+
- VideoTrack の有無判定を行うため、 MediaStream に `hasVideoTrack` を追加する
31+
- @t-miya
32+
- [ADD] MediaChannel に `setVideoHardMute(_:)` を追加する
33+
- 映像ハードミュート機能のシンタックスシュガー
34+
- CameraVideoCapturer を停止 / 再開し、カメラ入力を停止 / 再開する
35+
- 映像ハードミュート時は、映像ソフトミュートも併用する
36+
- @t-miya
2737
- [ADD] 音声のハードミュート有効化/無効化機能を追加する
2838
- iOS 端末のマイクインジケーターを消灯させる
2939
- AudioDeviceModuleWrapper クラスを追加する

Sora/MediaChannel.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ public final class MediaChannel {
233233

234234
private let manager: Sora
235235

236+
// 映像ハードミュートの同時呼び出しを防ぐためのキューです
237+
// 同時に呼び出された場合はエラーになります
238+
private let videoHardMuteSerialQueue = VideoHardMuteSerialQueue()
239+
236240
// MARK: - インスタンスの生成
237241

238242
/// 初期化します。
@@ -686,6 +690,87 @@ public final class MediaChannel {
686690
Logger.debug(type: .mediaChannel, message: "setAudioSoftMute mute=\(mute)")
687691
return nil
688692
}
693+
694+
/// MediaChannel の接続中に映像をソフトミュート有効化 / 無効化します
695+
/// 接続時のロールが sendonly または sendrecv でないとエラーを返します
696+
/// - Parameter mute: `true` で有効化、`false` で無効化
697+
/// - Returns: 成功した場合は `nil`、失敗した場合は `Error` を返します
698+
public func setVideoSoftMute(_ mute: Bool) -> Error? {
699+
guard state == .connected else {
700+
return SoraError.mediaChannelError(
701+
reason: "MediaChannel is not connected (state: \(state))")
702+
}
703+
704+
guard configuration.videoEnabled else {
705+
return SoraError.mediaChannelError(reason: "videoEnabled is false")
706+
}
707+
708+
guard configuration.isSender else {
709+
return SoraError.mediaChannelError(reason: "role is not sender")
710+
}
711+
712+
guard let senderStream else {
713+
return SoraError.mediaChannelError(reason: "senderStream is unavailable")
714+
}
715+
716+
guard senderStream.hasVideoTrack else {
717+
return SoraError.mediaChannelError(reason: "senderStream has no VideoTrack")
718+
}
719+
720+
senderStream.videoEnabled = !mute
721+
Logger.debug(type: .mediaChannel, message: "setVideoSoftMute mute=\(mute)")
722+
return nil
723+
}
724+
725+
/// MediaChannel の接続中に映像をハードミュート有効化 / 無効化します
726+
///
727+
/// ハードミュートは、カメラ入力を停止 / 再開します。
728+
/// `Configuration.cameraSettings.isEnabled == true` の場合のみ有効です。
729+
/// 内部でシリアルキューにより、操作を排他実行します。
730+
/// 同時に呼び出された場合はエラーになります。
731+
///
732+
/// 映像ハードミュートは、黒塗りフレーム状態で停止させるため映像ソフトミュート用処理を併用します。
733+
/// そのため、以下の処理を内部で実行します。
734+
///
735+
/// - `mute == true`: 映像ソフトミュートを有効化してから、カメラ入力を停止します
736+
/// - `mute == false`: カメラ入力を再開してから、映像ソフトミュートを無効化します。
737+
/// ハードミュート前のソフトミュートの状態に関わらず無効化します
738+
///
739+
/// - Parameter mute: `true` で有効化、`false` で無効化
740+
public func setVideoHardMute(_ mute: Bool) async throws {
741+
guard state == .connected else {
742+
throw SoraError.mediaChannelError(reason: "MediaChannel is not connected (state: \(state))")
743+
}
744+
745+
guard configuration.videoEnabled else {
746+
throw SoraError.mediaChannelError(reason: "videoEnabled is false")
747+
}
748+
749+
guard configuration.cameraSettings.isEnabled else {
750+
throw SoraError.mediaChannelError(reason: "cameraSettings.isEnabled is false")
751+
}
752+
753+
guard configuration.isSender else {
754+
throw SoraError.mediaChannelError(reason: "role is not sender")
755+
}
756+
757+
guard let senderStream else {
758+
throw SoraError.mediaChannelError(reason: "senderStream is unavailable")
759+
}
760+
761+
guard senderStream.hasVideoTrack else {
762+
throw SoraError.mediaChannelError(reason: "senderStream has no VideoTrack")
763+
}
764+
765+
if mute {
766+
senderStream.videoEnabled = false
767+
try await videoHardMuteSerialQueue.set(mute: true, senderStream: senderStream)
768+
} else {
769+
try await videoHardMuteSerialQueue.set(mute: false, senderStream: senderStream)
770+
senderStream.videoEnabled = true
771+
}
772+
Logger.debug(type: .mediaChannel, message: "setVideoHardMute mute=\(mute)")
773+
}
689774
}
690775

691776
extension MediaChannel: CustomStringConvertible {

Sora/MediaStream.swift

Lines changed: 14 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 hasVideoTrack: Bool { get }
66+
6267
/// 音声トラックを保持している場合は ``true`` を返します。
6368
///
6469
/// SDK 内部で利用する判定用のプロパティです。
@@ -206,6 +211,10 @@ class BasicMediaStream: MediaStream {
206211
nativeAudioTrack != nil
207212
}
208213

214+
var hasVideoTrack: Bool {
215+
nativeVideoTrack != nil
216+
}
217+
209218
var remoteAudioVolume: Double? {
210219
get {
211220
nativeAudioTrack?.source.volume
@@ -269,3 +278,8 @@ class BasicMediaStream: MediaStream {
269278
}
270279
}
271280
}
281+
282+
public extension MediaStream {
283+
var hasAudioTrack: Bool { false }
284+
var hasVideoTrack: Bool { false }
285+
}

Sora/VideoMute.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Foundation
2+
3+
// 映像ハードミュートの同時呼び出しを防ぐためのシリアルキュークラスです
4+
// 同時に呼び出された場合はエラーになります
5+
final class VideoHardMuteSerialQueue {
6+
private let queue = DispatchQueue(label: "jp.shiguredo.sora.video.hardmute")
7+
8+
private var isProcessing = false
9+
private var capturer: CameraVideoCapturer?
10+
11+
func set(mute: Bool, senderStream: MediaStream) async throws {
12+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
13+
queue.async { [self] in
14+
guard !isProcessing else {
15+
continuation.resume(
16+
throwing: SoraError.mediaChannelError(
17+
reason: "video hard mute operation is in progress"))
18+
return
19+
}
20+
21+
isProcessing = true
22+
let cachedCapturer = capturer
23+
24+
func finish(_ error: Error?, update: ((VideoHardMuteSerialQueue) -> Void)? = nil) {
25+
queue.async { [self] in
26+
update?(self)
27+
isProcessing = false
28+
if let error {
29+
continuation.resume(throwing: error)
30+
} else {
31+
continuation.resume(returning: ())
32+
}
33+
}
34+
}
35+
36+
SoraDispatcher.async(on: .camera) {
37+
if mute {
38+
guard let current = CameraVideoCapturer.current else {
39+
if cachedCapturer != nil {
40+
finish(nil)
41+
} else {
42+
finish(SoraError.mediaChannelError(reason: "CameraVideoCapturer is unavailable"))
43+
}
44+
return
45+
}
46+
47+
current.stop { error in
48+
finish(error) { serialQueue in
49+
serialQueue.capturer = current
50+
}
51+
}
52+
return
53+
}
54+
55+
if CameraVideoCapturer.current != nil {
56+
finish(nil)
57+
return
58+
}
59+
60+
guard let cachedCapturer else {
61+
finish(SoraError.mediaChannelError(reason: "CameraVideoCapturer is unavailable"))
62+
return
63+
}
64+
65+
cachedCapturer.stream = senderStream
66+
cachedCapturer.restart { error in
67+
finish(error)
68+
}
69+
}
70+
}
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)