@@ -224,6 +224,10 @@ public final class MediaChannel {
224224
225225 private let manager : Sora
226226
227+ // 映像ハードミュートの同時呼び出しを直列化するための Actor です
228+ // MediaChannel 間の排他実行を保証するため static にしています
229+ private static let videoHardMuteActor = VideoHardMuteActor ( )
230+
227231 // MARK: - インスタンスの生成
228232
229233 /// 初期化します。
@@ -632,18 +636,27 @@ public final class MediaChannel {
632636 }
633637
634638 /// MediaChannel の接続中にマイクをハードミュート有効化/無効化します
639+ ///
635640 /// - Parameter mute: `true` で有効化、`false` で無効化
636- /// - Returns: 成功した場合は `nil`、失敗した場合は `Error ` を返します
641+ /// - Returns: 成功した場合は `nil`、失敗した場合は `SoraError.mediaChannelError ` を返します
637642 public func setAudioHardMute( _ mute: Bool ) -> Error ? {
643+ // 接続されていなければエラー
638644 guard state == . connected else {
639645 return SoraError . mediaChannelError (
640646 reason: " MediaChannel is not connected (state: \( state) ) " )
641647 }
642648
649+ // 接続設定で音声が有効になっていなければエラー
643650 guard configuration. audioEnabled else {
644651 return SoraError . mediaChannelError ( reason: " audioEnabled is false " )
645652 }
646653
654+ // 接続設定で配信側ロールになっていなければエラー
655+ guard configuration. isSender else {
656+ return SoraError . mediaChannelError ( reason: " role is not sender " )
657+ }
658+
659+ // 音声ハードミュートを切り替えます
647660 if !NativePeerChannelFactory. default. audioDeviceModuleWrapper. setAudioHardMute ( mute) {
648661 return SoraError . mediaChannelError (
649662 reason: " AudioDeviceModuleWrapper::setAudioHardMute failed " )
@@ -653,30 +666,136 @@ public final class MediaChannel {
653666 }
654667
655668 /// MediaChannel の接続中にマイクをソフトミュート有効化 / 無効化します
669+ ///
656670 /// - Parameter mute: `true` で有効化、`false` で無効化
657- /// - Returns: 成功した場合は `nil`、失敗した場合は `Error ` を返します
671+ /// - Returns: 成功した場合は `nil`、失敗した場合は `SoraError.mediaChannelError ` を返します
658672 public func setAudioSoftMute( _ mute: Bool ) -> Error ? {
673+ // 接続されていなければエラー
659674 guard state == . connected else {
660675 return SoraError . mediaChannelError (
661676 reason: " MediaChannel is not connected (state: \( state) ) " )
662677 }
663678
679+ // 接続設定で音声が有効になっていなければエラー
664680 guard configuration. audioEnabled else {
665681 return SoraError . mediaChannelError ( reason: " audioEnabled is false " )
666682 }
667683
684+ // 接続設定で配信側ロールになっていなければエラー
685+ guard configuration. isSender else {
686+ return SoraError . mediaChannelError ( reason: " role is not sender " )
687+ }
688+
689+ // 送信ストリームが有効でなければエラー
668690 guard let senderStream else {
669691 return SoraError . mediaChannelError ( reason: " senderStream is unavailable " )
670692 }
671693
694+ // ローカル音声トラックが存在しなければエラー
672695 guard senderStream. hasAudioTrack else {
673696 return SoraError . mediaChannelError ( reason: " senderStream has no AudioTrack " )
674697 }
675698
699+ // ローカル音声トラックの有効/無効を切り替えます
676700 senderStream. audioEnabled = !mute
677701 Logger . debug ( type: . mediaChannel, message: " setAudioSoftMute mute= \( mute) " )
678702 return nil
679703 }
704+
705+ /// MediaChannel の接続中に映像をソフトミュート有効化 / 無効化します
706+ /// 黒塗りフレームが送信される状態になります
707+ ///
708+ /// - Parameter mute: `true` で有効化、`false` で無効化
709+ /// - Returns: 成功した場合は `nil`、失敗した場合は `SoraError.mediaChannelError` を返します
710+ public func setVideoSoftMute( _ mute: Bool ) -> Error ? {
711+ let senderStream : MediaStream
712+ switch requireSenderStreamForVideoMute ( ) {
713+ case . failure( let error) :
714+ return error
715+ case . success( let stream) :
716+ senderStream = stream
717+ }
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+ /// 外部入力や別キャプチャ経路には対応していません
729+ ///
730+ /// 内部で Actor により、操作を排他実行します。
731+ /// 同時に呼び出された場合は Actor 側で `SoraError.mediaChannelError` がスローされます
732+ ///
733+ /// 映像ハードミュートは、黒塗りフレーム状態で停止させるためローカルトラックの停止を含みます
734+ /// 事前に映像ソフトミュートを利用していた場合は状態が上書きされます
735+ /// ハードミュート解除時に直前のソフトミュートの状態を復元するようなことはしません
736+ ///
737+ /// - Parameter mute: `true` で有効化、`false` で無効化
738+ /// - Throws: エラー時は `SoraError.cameraError` または `SoraError.mediaChannelError` がスローされます
739+ public func setVideoHardMute( _ mute: Bool ) async throws {
740+ let senderStream : MediaStream
741+ switch requireSenderStreamForVideoMute ( ) {
742+ case . failure( let error) :
743+ throw error
744+ case . success( let stream) :
745+ senderStream = stream
746+ }
747+
748+ // 接続設定でカメラ利用が有効になっているか
749+ // 端末カメラではなく別ソース(外部入力や別キャプチャ経路)の場合は false になることがあり、機能としては未対応
750+ guard configuration. cameraSettings. isEnabled else {
751+ throw SoraError . mediaChannelError ( reason: " cameraSettings.isEnabled is false " )
752+ }
753+
754+ if mute {
755+ // ソフトミュートによる黒塗りフレーム送出 -> ハードミュート有効化の順になるようにします
756+ senderStream. videoEnabled = false
757+ try await Self . videoHardMuteActor. setMute ( mute: true , senderStream: senderStream)
758+ } else {
759+ // ハードミュート無効化 -> ソフトミュートによる黒塗りフレーム送出解除の順になるようにします
760+ try await Self . videoHardMuteActor. setMute ( mute: false , senderStream: senderStream)
761+ senderStream. videoEnabled = true
762+ }
763+ Logger . debug ( type: . mediaChannel, message: " setVideoHardMute mute= \( mute) " )
764+ }
765+
766+ // 映像ミュートのための接続状況や接続設定のチェックを実行した上で送信ストリームを取得します
767+ //
768+ // チェックを全て通過した場合は .success で送信ストリームを返します
769+ // 問題があった場合は .failure で SoraError.mediaChannelError を返します
770+ private func requireSenderStreamForVideoMute( ) -> Result < MediaStream , Error > {
771+ // 接続されていなければエラー
772+ guard state == . connected else {
773+ return . failure(
774+ SoraError . mediaChannelError ( reason: " MediaChannel is not connected (state: \( state) ) " ) )
775+ }
776+
777+ // 接続設定で映像が有効になっていなければエラー
778+ guard configuration. videoEnabled else {
779+ return . failure( SoraError . mediaChannelError ( reason: " videoEnabled is false " ) )
780+ }
781+
782+ // 接続設定で配信側ロールになっていなければエラー
783+ guard configuration. isSender else {
784+ return . failure( SoraError . mediaChannelError ( reason: " role is not sender " ) )
785+ }
786+
787+ // 送信ストリームが有効になっていなければエラー
788+ guard let senderStream else {
789+ return . failure( SoraError . mediaChannelError ( reason: " senderStream is unavailable " ) )
790+ }
791+
792+ // 送信ストリームに映像トラックが含まれていなければエラー
793+ guard senderStream. hasVideoTrack else {
794+ return . failure( SoraError . mediaChannelError ( reason: " senderStream has no VideoTrack " ) )
795+ }
796+
797+ return . success( senderStream)
798+ }
680799}
681800
682801extension MediaChannel : CustomStringConvertible {
0 commit comments