@@ -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
691776extension MediaChannel : CustomStringConvertible {
0 commit comments