developer
interactivity
How-to Mute Participants in a Conference
Fabien Lavocat

A question that I get a lot is: “How do I mute all participants of a conference remotely?” Let’s take the scenario where you are hosting a presentation and of course there is always that one person who forgets to mute the microphone while doing something else (that’s usually me…) and creates unwanted noise in the conference. How do you take action to handle this situation?

It is possible to mute a remote participant (this capability for Dolby Voice came with our SDK 3.2) release. However, the effect is only on yourself, meaning that if you mute one particular participant, everybody but you will still be able to hear that person, so this is not what we want to do here.

We recommend leveraging the CommandService (available on all platforms) to send messages to the participants in a conference. These messages can be a simple string as well as a JSON payload (stringified).

Standard message

The broadcasted messages can have multiple receivers in your application. You can send messages to use for chat communication, you can trigger commands to manipulate the user interface for the participants, or you can spread the local status of the participant to others (audio and video state…). We can create a standard format for all the messages that will be sent and received through the CommandService. We will use a JSON message that contains a property called command as the name of the action we want to transmit with this message, as well as a content property that will contain all the information necessary for the action to be performed.

Message examples

As an example, this could be the payload to use to send a chat message to the other participants of the conference:

{
    "command": "ChatMessage",
    "content": {
        "text": "Hello world!"
    }
}

What is interesting to us in this blog post is for the host to be able to mute remote participants. So, to mute all of them at once, we can use this payload:

{
    "command": "Mute",
    "content": {
        "participants": []
    }
}

In the event that we want to mute only a selected few participants, because the CommandService will broadcast the message to all the participants, we need to specify the list of participants we want to mute and filter the message if needed:

{
    "command": "Mute",
    "content": {
        "participants": [
            "externalId01",
            "externalId02",
            "externalId03"
        ]
    }
}

The same applies to unmute some participants:

{
    "command": "Unmute",
    "content": {
        "participants": [
            "externalId04",
            "externalId05"
        ]
    }
}

Send a message

We will take the example of muting some participants in the conference. First, we need to build the JSON payload and then use the CommandService to send these messages across the conference.

Web SDK

In JavaScript, we will use the CommandService to send the message:

// Build your list of participants you want to mute
const participants = [
    "externalId01",
    "externalId02",
    "externalId03"
]

// Build the JSON payload to send to the participants
const payload = {
    "command": "Mute",
    "content": {
        "participants": participants
    }
}

// The content of the message must be a string
const json = JSON.stringify(payload);

// Send the message to the other participants
VoxeetSDK
    .command
    .send(json)
    .then(() => console.log("Message sent successfully."))
    .catch((error) => console.error("There was an error sending the message.", error));

Swift for iOS

In Swift for iOS with the CommandService:

// Build your list of participants you want to mute
let participants = [
    "externalId01",
    "externalId02",
    "externalId03"
]

// Build the JSON payload to send to the participants
let payload = [
    "command": "Mute",
    "content": [
        "participants": participants
    ]
] as [String: Any]

// The content of the message must be a string
if let jsonData = try? JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted),
   let jsonString = String(data: jsonData, encoding: .utf8) {
    // Send the message to the other participants
    VoxeetSDK.shared.command.send(message: jsonString, completion: { error in
        if error == nil {
            // The message was properly sent to the conference
        }
    })
}

Java for Android

In Java for Android with the CommandService:

try {
    // Build your list of participants you want to mute
    JSONArray participants = new JSONArray();
    participants.put("externalId01");
    participants.put("externalId02");
    participants.put("externalId03");

    JSONObject jObjectContent = new JSONObject();
    jObjectContent.put("participants", participants);

    // Build the JSON payload to send to the participants
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("command", "Mute");
    jsonObject.put("content", jObjectContent);

    String jsonStr = jsonObject.toString(4);
    String conferenceId = VoxeetSDK.conference().getCurrentConference().getConference().getId();

    // Send the message to the other participants
    VoxeetSDK.command().send(conferenceId, jsonStr)
            .then((r) -> { /* Success */ })
            .error((error) -> { /* Handle error */ });

} catch (JSONException e) {
    // Handle exception
}

The message is now broadcasted to all the participants, let’s now work on processing the message on the receiver side.

Process a message received

In your application, we need to subscribe to the received event of the CommandService, then process the incoming messages and trigger actions if needed.

Web SDK

// Subscribe to the received event
VoxeetSDK.command.on("received", (participant, message) => {
    console.info("Message/Command received from", participant);

    const json = JSON.parse(message);
    if (json.command === "Mute") {
        if (Voxeet.conference.isMuted || Voxeet.session.participant.type !== "user") {
            // The local participant is already muted or is a listener
            return;
        }

        if (!json.content || json.content.participants // If the participants property is missing
            || json.content.participants.length <= 0 // or the list is empty (we would consider we want everybody to be muted)
            || json.content.participants.indexOf(Voxeet.session.participant.id) >= 0) { // the participant needs to be muted
            // Mute the local participant
            Voxeet.conference.mute(Voxeet.session.participant, true);
        }
    } else if (json.command === "Unmute") {
        if (!Voxeet.conference.isMuted || Voxeet.session.participant.type !== "user") {
            // The local participant is already unmuted or is a listener
            return;
        }

        if (!json.content || json.content.participants // If the participants property is missing
            || json.content.participants.length <= 0 // or the list is empty (we would consider we want everybody to be unmuted)
            || json.content.participants.indexOf(Voxeet.session.participant.id) >= 0) { // the participant needs to be unmuted
            // Unmute the local participant
            Voxeet.conference.mute(Voxeet.session.participant, false);
        }
    }
});

Swift for iOS

We are going to use the VTCommandDelegate in Swift.

extension ViewController: VTCommandDelegate {
    
    func received(participant: VTParticipant, message: String) {
        if let participantId = participant.id {
            print("Message/Command received from " + participantId)
        }
        
        if VoxeetSDK.shared.session.participant?.type != VTParticipantType.user {
            // The local participant must be a user to trigger actions
            return
        }
        
        guard let externalId = VoxeetSDK.shared.session.participant?.info.externalID else {
            // We need an externalId for the local participant
            return
        }
        
        if let jsonData = message.data(using: .utf8, allowLossyConversion: false),
           let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []),
           let dico = jsonObject as? NSDictionary,
           let actionName = dico.value(forKey: "command") as? String,
           let content = dico.value(forKey: "content") as? NSDictionary {
            
            if actionName == "Mute" {
                if (VoxeetSDK.shared.conference.isMuted()) {
                    // The local participant is already muted
                    return
                }
                
                if let participants = content.value(forKey: "participants") as? Array<String> {
                    if participants.isEmpty // if the list is empty (we would consider we want everybody to be muted)
                        || participants.contains(externalId) { // the participant needs to be muted
                        // Mute the local participant
                        VoxeetSDK.shared.conference.mute(true)
                    }
                }
            } else if actionName == "Unmute" {
                if (!VoxeetSDK.shared.conference.isMuted()) {
                    // The local participant is already unmuted
                    return
                }
                
                if let participants = content.value(forKey: "participants") as? Array<String> {
                    if participants.isEmpty // if the list is empty (we would consider we want everybody to be muted)
                        || participants.contains(externalId) { // the participant needs to be muted
                        // Unmute the local participant
                        VoxeetSDK.shared.conference.mute(false)
                    }
                }
            }
        }
    }
    
}

Now assign the command delegate to the current ViewController:

VoxeetSDK.shared.command.delegate = self

Java for Android

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

private static boolean contains(JSONArray jArray, String value) throws JSONException {
    for (int i = 0; i < jArray.length(); i++) {
        if (jArray.getString(i).equalsIgnoreCase(value)) {
            return true;
        }
    }

    return false;
}

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(MessageReceived event) {
    Log.d(TAG, "Message/Command received from " + event.participantId);

    try {
        JSONObject jsonObject = new JSONObject(event.message);
        String command = jsonObject.getString("command");
        if ("Mute".equalsIgnoreCase(command)) {
            if (VoxeetSDK.conference().isMuted() || VoxeetSDK.session().getParticipant().participantType() != ParticipantType.USER) {
                // The local participant is already muted or is a listener
                return;
            }

            JSONObject jContent = jsonObject.getJSONObject("content");
            if (jContent == null) {
                // Mute the local participant
                VoxeetSDK.conference().mute(true);
            } else {
                JSONArray jParticipants = jContent.getJSONArray("participants");
                if (jParticipants == null // If the participants property is missing
                        || jParticipants.length() <= 0 // or the list is empty (we would consider we want everybody to be muted)
                        || contains(jParticipants, VoxeetSDK.session().getParticipantInfo().getExternalId())) { // the participant needs to be muted
                    // Mute the local participant
                    VoxeetSDK.conference().mute(true);
                }
            }
        } else if ("Unmute".equalsIgnoreCase(command)) {
            if (!VoxeetSDK.conference().isMuted() || VoxeetSDK.session().getParticipant().participantType() != ParticipantType.USER) {
                // The local participant is already unmuted or is a listener
                return;
            }

            JSONObject jContent = jsonObject.getJSONObject("content");
            if (jContent == null) {
                // Unmute the local participant
                VoxeetSDK.conference().mute(false);
            } else {
                JSONArray jParticipants = jContent.getJSONArray("participants");
                if (jParticipants == null // If the participants property is missing
                        || jParticipants.length() <= 0 // or the list is empty (we would consider we want everybody to be unmuted)
                        || contains(jParticipants, VoxeetSDK.session().getParticipantInfo().getExternalId())) { // the participant needs to be unmuted
                    // Unmute the local participant
                    VoxeetSDK.conference().mute(false);
                }
            }
        }
    } catch (JSONException e) {
        // Handle exception
    }
}

Bonus question

To conclude this post I also want to answer the following question, for free! What is the difference between stop/start audio and mute/unmute?

In a Dolby.io conference, when you mute yourself, the audio stream between your device and the media server is kept alive, but the SDK will send an empty audio track. When you unmute yourself, then the SDK will resume sending the audio to conference right away without lag. Now, if you stop the audio, the SDK will close the connection to the media server completely and when you start the audio again, a WebRTC re-negotiation will happen before the SDK is able to send the audio to the media server again. This can take a few hundred milliseconds.

Tags: android, ios, javascript
RELATED POSTS
DEVELOPER
MEDIA
How to Add Quality Assurance to Educational Video Production with Next.js

In this tutorial, learn how to automate this quality assurance process with Dolby.io and Next.js.

Daniel Latimer
|
nextjs
react
DEVELOPER
INTERACTIVITY
Set up a Live Stream with Dolby.io and Twitch

Use RTMP to set up a live stream with Dolby.io and Twitch.

Fabien Lavocat
|
rtmp
DEVELOPER
MEDIA
Generating Pre-Signed URLs for Azure Cloud Storage with Python

A getting started guide for integrating Azure cloud storage with Dolby.io’s media processing suite in Python through pre-signed URLs and shared access signatures.

Braden Riggs
|
azure
We're happy to chat about our APIs, SDKs...or magic.