ミュート機能

アプリを利用している Android 端末からの音声および映像の配信をミュートする機能です。

用語

プライバシーインジケーター

Android アプリがマイクデバイスまたはカメラデバイスを利用している場合に、ユーザーに対してデバイスが使用中であることを通知するためのインジケーターです。 マイク使用時はマイクアイコン、カメラ時はカメラアイコンがそれぞれ表示され、しばらくすると緑色のドットの表示に切り替わります。

このインジケーターはマイクまたはカメラの片方のみが使用中の場合でも表示され続けます。

プライバシーインジケーターは Android 12 から、デバイス使用時にステータスバーへの表示が必須になっています。

Tip

Android 12 より前の機種においてはプライバシーインジケーターを実装するかを含め任意でした。

プライバシーインジケーターの詳細については Android ドキュメント https://source.android.com/docs/core/permissions/privacy-indicators?hl=ja をご確認ください。

ソフトミュート

プライバシーインジケーターが点灯したままの音声・映像ミュートです。マイクデバイスおよびカメラデバイスは使用中の状態となっています。 音声の場合は無音フレーム、映像の場合は黒塗りフレームを送出し続けている状態です。

ハードミュート

プライバシーインジケーターが消灯する音声・映像ミュートです。マイクデバイスおよびカメラデバイスは使用されていない状態であり、 音声および映像はフレーム送出自体がされない状態です。

推奨するミュート方式について

特別な理由がない限りはハードミュートを使用してください。

ハードミュート有効化時はプライバシーインジケーターが消灯します。 これはマイクデバイスおよびカメラデバイスからのデータ送信が完全に停止されたことを意味し、 ユーザーに対して物理的なデバイスアクセスが行われていないことを確実に伝えることができます。 プライバシー保護の観点から、最も安全な選択肢です。

ハードミュートのデメリット

ハードミュートを利用する際のデメリットは特にありません。

音声のハードミュート

音声のハードミュート有効化

Sora Android SDK で音声のハードミュートを有効にするには SoraMediaChannel.setAudioRecordingPaused(boolean enable) を利用します。 このメソッドの引数に true を指定することで、マイクデバイスの録音を無効にします。 このタイミングで音声が送られ続けなくなるため、Android 端末のマイクインジケーターが消灯します。

また、マイクデバイスの音声が送られてこなくなるため、実際に音声パケットを一切送らなくなります。

音声のハードミュート無効化

音声のハードミュート有効化 で有効化したハードミュートを無効にするには SoraMediaChannel.setAudioRecordingPaused(boolean enable) を利用します。 このメソッドの引数に false を指定することで、マイクデバイスの録音を再開します。 このタイミングで音声が送られるようになるため、Android 端末のマイクインジケーターが点灯します。

また、音声パケットの送出も再開されます。

音声のハードミュート機能実装サンプル

/**
 * AudioHardMuteSampleActivity に
 * ハードミュート有効化ボタンイベント onMicHardMuteButtonClicked と
 * ハードミュート無効化ボタンイベント onMicHardUnMuteButtonClicked
 * を実装したサンプルです。
 *
 * kotlinx.coroutines の CoroutineScope, Dispatchers, launch, withContext を利用しています。
 * また、lifecycleScope を使用するためには androidx.lifecycle:lifecycle-runtime-ktx の導入が必要です。
 */

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import jp.shiguredo.sora.sdk.channel.SoraMediaChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/**
 * 音声ハードミュートの有効/無効化を制御するコントローラーのサンプルクラスです。
 * `SoraMediaChannel` の `setAudioRecordingPaused` 関数を非同期実行します。
 * `setAudioRecordingPaused` は suspend 関数として定義されています。
 *
 * 前提:
 * - 呼び出し側で `SoraMediaChannel` の生成と破棄を管理していること
 * - `scope` には UI スレッドをブロックしない `CoroutineScope` (例: `lifecycleScope` や `viewModelScope`) を渡していること
 *
 * `setHardMuted(true)` を呼び出すと、ハードミュートが有効になりマイクインジケーターが消灯します。
 */
class AudioHardMuteController(
    private val mediaChannel: SoraMediaChannel,
    private val scope: CoroutineScope,
) {
    fun setHardMuted(muted: Boolean) {
        scope.launch {
            val success = withContext(Dispatchers.Default) {
                mediaChannel.setAudioRecordingPaused(muted)
            }
            if (!success) {
                // 必要に応じて UI へエラーを通知する実装等を追加します
            }
        }
    }
}

/**
 * 音声ハードミュート用のサンプルアクティビティです
 * mediaChannel の生成についてはダミーの実装のためこのままではコンパイルは通りません
 */
class AudioHardMuteSampleActivity : AppCompatActivity() {
    private lateinit var mediaChannel: SoraMediaChannel
    private lateinit var controller: AudioHardMuteController

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mediaChannel = createDummyMediaChannel()
        controller =
            AudioHardMuteController(mediaChannel, lifecycleScope)
    }

    // ハードミュートを有効にするボタンイベントです
    fun onMicHardMuteButtonClicked() {
        controller.setHardMuted(true)
    }

    // ハードミュートを無効にするボタンイベントです
    fun onMicHardUnMuteButtonClicked() {
        controller.setHardMuted(false)
    }

    // ダミーの MediaChannel 生成メソッドです
    private fun createDummyMediaChannel(): SoraMediaChannel {
        // 実際のアプリでは SoraMediaChannel.Builder などで生成してください
        TODO("SoraMediaChannel を生成して返す")
    }
}

注釈

Android 端末および Android OS の割り込み等によるマイクデバイスの状態によっては録音スレッドの停止・再開が失敗することがあります。そのため Sora Android SDK 内部の setAudioRecordingPaused 処理ではローカル音声トラックの停止も併せて行うことで、不意の音声の送信を防ぐようにしています。

注釈

音声アップストリームが有効化( enableAudioUpstream() )されていない場合、 setAudioRecordingPaused は何も行わず、 true を返します。

Tip

アプリの実装例については sora-android-sdk-samples をご確認ください。

setAudioRecordingPaused の補足

音声ハードミュートは内部処理として、Sora Android SDK が利用している libwebrtc 内で録音スレッドの停止・再開を行なっています。

録音スレッドの停止・再開はスレッド同期に時間がかかることがあります。UI スレッドでの実行はアプリ応答不能となる可能性があるため setAudioRecordingPaused はワーカースレッドでの実行を前提としています。

注釈

録音スレッドの同期待ちについて、libwebrtc 内では 2 秒、SDK 内では 3 秒のタイムアウトを設けています。

外部 AudioDeviceModule 利用時の注意

SoraAudioOption.audioDeviceModule を設定していて外部から AudioDeviceModule を差し込んでいる場合、 setAudioRecordingPaused は動作しません。この場合は、独自に AudioDeviceModule を停止する処理を実装する必要があります。

音声のソフトミュート

通信処理によらないクライアントレイヤーの音声ミュートです。

音声のソフトミュート有効化

Sora Android SDK で音声のソフトミュートを有効にするには AudioTrack.setEnabled(boolean enable) を利用します。 このメソッドの引数に false を指定することで、音声トラックを無効にします。 このときマイクデバイスからは音声が送られ続けている状態のままのため、Android 端末のマイクインジケーターは点灯したままになります。

また、マイクデバイスからの音声は配信されませんがソフトミュートは実際にはパケットを送っており、無音のフレーム(デジタルサイレンスパケット)を送り続けています。

音声のソフトミュート無効化

音声のソフトミュート有効化 で有効化したソフトミュートを無効にするには AudioTrack.setEnabled(boolean enable) を利用します。 このメソッドの引数に true を指定することで音声トラックが有効になり、マイクデバイスの音声がパケットとして送信されるようになります。

音声のソフトミュート機能実装サンプル

/**
 * MediaStream から音声トラックを取得し、
 * ソフトミュート有効化ボタンイベント onMicSoftMuteButtonClicked と
 * ソフトミュート無効化ボタンイベント onMicSoftUnMuteButtonClicked
 * を実装したサンプルです。
 */

import jp.shiguredo.sora.sdk.channel.SoraMediaChannel
import org.webrtc.AudioTrack
import org.webrtc.MediaStream

/**
 * 音声ソフトミュートの有効/無効化を制御するコントローラーのサンプルクラスです。
 * onAddLocalStream で渡される MediaStream から AudioTrack を取得して保持します。
 */
class AudioSoftMuteController {
    private var localAudioTrack: AudioTrack? = null

    // MediaStream から音声トラックを取得します
    // Sora のルールとしてトラックは1本のみのため先頭要素がローカルの音声トラックになります
    fun onAddLocalStream(stream: MediaStream) {
        localAudioTrack = stream.audioTracks.firstOrNull()
    }

    // ソフトミュートを有効化/無効化します
    fun setSoftMuted(muted: Boolean) {
        localAudioTrack?.setEnabled(!muted)
    }
}

/**
 * AudioSoftMuteController の利用例です。
 * onAddLocalStream でローカル音声トラックを取得します。
 */
class AudioSoftMuteSample {
    private val audioSoftMute = AudioSoftMuteController()

    val listener = object : SoraMediaChannel.Listener {
        // ローカルストリームが追加されたときに呼び出されるコールバックです
        // ここで AudioSoftMuteController に MediaStream を渡してローカルの音声トラックを取得させます
        override fun onAddLocalStream(mediaChannel: SoraMediaChannel, stream: MediaStream) {
            audioSoftMute.onAddLocalStream(stream)
        }

        // その他コールバックの実装については省略します
    }

    // ソフトミュートを有効にするボタンイベントです
    fun onMicSoftMuteButtonClicked() {
        audioSoftMute.setSoftMuted(true)
    }

    // ソフトミュートを無効にするボタンイベントです
    fun onMicSoftUnmuteButtonClicked() {
        audioSoftMute.setSoftMuted(false)
    }
}

Tip

SoraMediaChannel.Listener の実装例については シグナリング をご確認ください。

Tip

アプリの実装例については sora-android-sdk-samples をご確認ください。

映像のハードミュート

映像のハードミュート有効化

Sora Android SDK でハードミュートを有効にするには CameraVideoCapturer.stopCapture() を利用します。 このメソッドを実行することで、カメラデバイスからのフレーム取得(キャプチャ)が停止します。 このタイミングで映像が送られ続けなくなるため、Android 端末のカメラインジケーターが消灯します。

また、カメラデバイスからの映像が送られてこなくなるため、実際に映像パケットを一切送らなくなります。

映像のソフトミュートを併用する

ハードミュートを有効にする前にソフトミュートを有効にして黒塗りフレームを送信する状態にしておくと安全です。 理由として、受信側の実装によってはパケット断が通信障害なのかハードミュートによる停止なのか判断できず、 停止時点の映像フレームを表示し続けてしまうケースがあるためです。

映像のハードミュート無効化

映像のハードミュート有効化 で有効化したハードミュートを無効にするには CameraVideoCapturer.startCapture() を利用します。 このメソッドを実行することで、カメラデバイスからのフレーム取得が開始されます。 このタイミングで Android 端末のカメラインジケーターが点灯します。

また、映像パケットの送出も再開されます。

ハードミュート有効化の際にソフトミュートも有効にしていた場合は、ソフトミュートの無効化も併せて行う必要があります。

映像のハードミュート機能実装サンプル

/**
 * VideoHardMuteSampleActivity に
 * ハードミュート有効化ボタンイベント onCameraHardMuteButtonClicked と
 * ハードミュート無効化ボタンイベント onCameraHardUnMuteButtonClicked
 * を実装したサンプルです。
 *
 * kotlinx.coroutines の CoroutineScope, Dispatchers, launch, withContext を利用しています。
 * また、lifecycleScope を使用するためには androidx.lifecycle:lifecycle-runtime-ktx の導入が必要です。
 */
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import jp.shiguredo.sora.sdk.channel.SoraMediaChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.webrtc.CameraVideoCapturer
import org.webrtc.MediaStream
import org.webrtc.VideoTrack

/**
 * 映像ハードミュートの有効/無効化を制御するコントローラーのサンプルクラスです。
 * `CameraVideoCapturer` の startCapture/stopCapture 関数を非同期実行します。
 * startCapture/stopCapture 関数はブロッキング処理のため非同期で実行する必要があります。
 * また、ソフトミュートを併せて有効にすることでキャプチャ停止前のフレームを黒塗りフレームにします。
 * 受信するクライアント側の挙動として、映像パケットが送られてこない場合は直前のフレームを表示し続けるケースがあるためです。
 *
 * 前提:
 * - `capturer` は `SoraMediaOption#videoCapturerFactory` などから取得していること
 * - `scope` は UI スレッドをブロックしない `CoroutineScope` (例: `lifecycleScope` や `viewModelScope`) を渡すこと
 * - `startCapture` 用の `videoWidth` / `videoHeight` / `videoFPS` はキャプチャ開始時に利用した値を保持していること
 *
 * `setHardMuted(true)` を呼び出すと、ハードミュートが有効になりカメラインジケーターが消灯します。
 */
class VideoHardMuteController(
    private val capturer: CameraVideoCapturer,
    private val scope: CoroutineScope,
    private val videoWidth: Int,
    private val videoHeight: Int,
    private val videoFPS: Int,
) {
    private var localVideoTrack: VideoTrack? = null

    // MediaStream からローカル映像トラックを取得します
    // Sora のルールとして 1 ストリームにトラックは 1 本のみのため先頭要素がローカル映像トラックです
    fun onAddLocalStream(stream: MediaStream) {
        localVideoTrack = stream.videoTracks.firstOrNull()
    }

    // クリーンアップ処理です
    fun clearLocalVideoTrack() {
        localVideoTrack = null
    }

    // 映像ハードミュートの有効・無効を切り替えます
    //
    // ハードミュート有効化時
    // 1. 映像トラックを無効にして黒塗りフレームが送信される状態にします
    // 2. その後キャプチャの停止を行います
    // トラックとキャプチャの切り替えは同一コルーチン内で順番に実行します。
    // 先にトラックを無効化して黒塗りフレームを送出してからキャプチャを停止することで、
    // 黒塗りフレームが送信される前にキャプチャが止まってしまう状況を避けます。
    // VideoTrack.setEnabled() にはブロッキング処理はないためメインスレッド上での実行でも問題ありません。
    //
    // ハードミュート無効化時
    // 1. キャプチャの開始を行い映像パケットが送信される状態にします
    // 2. 映像トラックを有効にしてカメラデバイスからの映像が送信される状態にします
    // 有効化時とは逆順に処理します。
    fun setHardMuted(muted: Boolean) {
        scope.launch {
            runCatching {
                if (muted) {
                    withContext(Dispatchers.Main) {
                        localVideoTrack?.setEnabled(false)
                    }
                }
                withContext(Dispatchers.Default) {
                    if (muted) {
                        capturer.stopCapture()
                    } else {
                        capturer.startCapture(videoWidth, videoHeight, videoFPS)
                    }
                }
                if (!muted) {
                    withContext(Dispatchers.Main) {
                        localVideoTrack?.setEnabled(true)
                    }
                }
            }.onFailure {
                // 必要に応じて UI へエラーを通知する実装等を追加します
            }
        }
    }
}

/**
 * 映像ハードミュート用のサンプルアクティビティです
 * CameraVideoCapturer の生成についてはダミーの実装のためこのままではコンパイルは通りません
 */
class VideoHardMuteSampleActivity : AppCompatActivity() {
    private lateinit var capturer: CameraVideoCapturer
    private lateinit var controller: VideoHardMuteController
    // 実際のアプリでは生成した SoraMediaChannel にこの listener を登録してください
    private val channelListener = object : SoraMediaChannel.Listener {
        // ローカルストリームが追加されたときに呼び出されるコールバックです
        // ここで VideoHardMuteController に MediaStream を渡してローカルの映像トラックを取得させます
        override fun onAddLocalStream(mediaChannel: SoraMediaChannel, stream: MediaStream) {
            controller.onAddLocalStream(stream)
        }

        // 切断時のコールバックです。VideoHardMuteController のクリーンアップ処理を行います
        // 実際のアプリでは必要に応じて他モジュールのクリーンアップも行います
        override fun onDisconnect(mediaChannel: SoraMediaChannel) {
            controller.clearLocalVideoTrack()
        }

        // その他のコールバックについては省略します
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        capturer = createDummyCapturer()
        // 例として、Widht:1280。Height: 720、FPS: 30 の設定にします
        // ここではサンプルとして固定値としていますが、実際の実装ではミュート前と同じ状態で映像再開するために
        // あらかじめ変数やプロパティ等で設定値を保持しておく必要があります
        controller =
            VideoHardMuteController(capturer, lifecycleScope, 1280, 720, 30)
    }

    // ハードミュートを有効にします
    fun onCameraHardMuteButtonClicked() {
        controller.setHardMuted(true)
    }

    // ハードミュートを無効にします
    fun onCameraHardUnmuteButtonClicked() {
        controller.setHardMuted(false)
    }

    // ダミーの CameraVideoCapturer 生成メソッドです
    private fun createDummyCapturer(): CameraVideoCapturer {
        // 実際のアプリでは CameraVideoCapturerFactory などから取得してください
        TODO("CameraVideoCapturer を生成して返す")
    }
}

注釈

VideoTrack.setEnabled(false) を先に実行して黒塗りフレームを送出する状態にした後にハードミュートを有効にしています。復帰時はハードミュートを無効化した後に setEnabled(true) でカメラデバイスからのフレーム送出を再開します。 詳細は 映像のソフトミュートを併用する をご確認ください。

Tip

アプリの実装例については sora-android-sdk-samples をご確認ください。

カメラキャプチャラーの取得方法について

サンプルコード中ではダミー実装としているカメラキャプチャラーの取得方法については カメラの映像を取得する をご確認ください。

startCapture/stopCapture を実行するスレッドについて

CameraVideoCapturer.startCapture()/stopCapture() は Android 端末およびカメラデバイスの状況によってはブロッキング処理となるため、 アプリ応答不能を防ぐために UI スレッドではなくワーカースレッド上で実行するようにしてください。

映像のソフトミュート

通信処理によらないクライアントレイヤーの映像ミュートです。

映像のソフトミュート有効化

Sora Android SDK で映像のソフトミュートを有効にするには VideoTrack.setEnabled(boolean enable) を利用します。 このメソッドを false を指定することで、映像トラックを無効にします。

このときカメラデバイスからは映像が送られ続けている状態のままのため、Android 端末のカメラインジケーターは点灯したままになります。

また、カメラデバイスからの映像は配信されませんが、ソフトミュートは実際にはパケットを送っており、黒塗りのフレームを送り続けています。

下画像は実際に Android 端末でソフトミュートを有効にした状態での配信を Sora DevTools で受信した際のものです。

https://i.gyazo.com/00bf75c3c29722e801932f05bddd9c76.png

映像のソフトミュート無効化

映像のソフトミュート有効化 で有効化したソフトミュートを無効にするには VideoTrack.setEnabled(boolean enable) を利用します。 このメソッドを true を指定することで映像トラックが有効になり、カメラデバイスの映像がパケットとして送信されるようになります。

映像のソフトミュート機能実装サンプル

/**
 * MediaStream から映像トラックを取得し、
 * ソフトミュート有効化ボタンイベント onMicSoftMuteButtonClicked と
 * ソフトミュート無効化ボタンイベント onMicSoftUnMuteButtonClicked
 * を実装したサンプルです。
 */

import jp.shiguredo.sora.sdk.channel.SoraMediaChannel
import org.webrtc.VideoTrack
import org.webrtc.MediaStream

/**
 * 映像ソフトミュートの有効/無効化を制御するコントローラーのサンプルクラスです。
 * onAddLocalStream で渡される MediaStream から VideoTrack を取得して保持します。
 */
class VideoSoftMuteController {
    private var localVideoTrack: VideoTrack? = null

    // MediaStream から映像トラックを取得します
    // Sora のルールとしてトラックは1本のみのため先頭要素がローカルの映像トラックになります
    fun onAddLocalStream(stream: MediaStream) {
        localVideoTrack = stream.videoTracks.firstOrNull()
    }

    // ソフトミュートを有効化/無効化します
    fun setSoftMuted(muted: Boolean) {
        localVideoTrack?.setEnabled(!muted)
    }
}

/**
 * VideoSoftMuteController の利用例です。
 * onAddLocalStream でローカル映像トラックを取得します。
 */
class VideoSoftMuteSample {
    private val controller = VideoSoftMuteController()

    val listener = object : SoraMediaChannel.Listener {
        // ローカルストリームが追加されたときに呼び出されるコールバックです
        // ここで VideoSoftMuteController に MediaStream を渡してローカルの映像トラックを取得させます
        override fun onAddLocalStream(mediaChannel: SoraMediaChannel, stream: MediaStream) {
            controller.onAddLocalStream(stream)
        }

        // その他コールバックの実装については省略します
    }

    // ソフトミュートを有効にするボタンイベントです
    fun onCameraSoftMuteButtonClicked() {
        videoSoftMute.setSoftMuted(true)
    }

    // ソフトミュートを無効にするボタンイベントです
    fun onCameraSoftUnmuteButtonClicked() {
        videoSoftMute.setSoftMuted(false)
    }
}

Tip

SoraMediaChannel.Listener の実装例については シグナリング もご確認ください。

Tip

アプリの実装例については sora-android-sdk-samples をご確認ください。

プライバシーインジケーターの反映について

マイクデバイスとカメラデバイスのハードミュートを有効にした際、プライバシーインジケーターの表示が完全に消えるまでのタイミングは Android OS による制御です。配信パケット送出が停止した後も数秒間はプライバシーインジケーターの表示が残ることがあります。

詳細については Android ドキュメントのプライバシーインジケーター https://source.android.com/docs/core/permissions/privacy-indicators?hl=ja をご確認ください。

Sora 録画機能とハードミュートの併用について

Sora の録画機能での録画中に映像ハードミュートを有効にした場合、映像パケットが送られない状態になるため、停止時点でのフレームが録画され続けます。

映像のハードミュート有効化 でも述べたようにソフトミュート有効化による黒塗りフレームの送出状態を挟むことで動画として不自然になることを防ぐことができます。

また、音声と映像の両方がハードミュート有効の状態で録画が開始され、ハードミュートを無効にすることなく録画終了した場合、録画用のパケットが一切送信されていないため録画ファイル自体が出力されません。

Sora の録画機能については https://sora-doc.shiguredo.jp/RECORDING をご確認ください。

© Copyright 2018-2025, Shiguredo Inc. Created using Sphinx 8.2.3