Changing Layouts

SignalWire video calls are composited at SignalWire servers so that everyone gets the same video feed. This means that layout changes are reflected instantly in each participant’s video stream. Each participant sees exactly the same video layout once the layout switch occurs.

Necessary Permissions

Changing layouts is a privilege that you must explicitly request when asking the SignalWire Video REST API for an access token. The endpoint to request access tokens is described in the SignalWire Video API reference guide.

In particular, you need to specify the room.set_layout permission and the room.list_available_layouts permission when requesting the token. This should happen in the server-side, ideally after you verify that the user requesting the access token actually has the privilege to change your video call's layout.

We have already done this in the previous part. When we defined the permissions for moderator in the backend, we included the two permissions:

// Extracted from the backend we wrote in the previous part:
const moderatorPermissions = [
  // These two permissions enable us to 
  // switch layouts
  "room.list_available_layouts",
  "room.set_layout",
  // ... other permissions
];
curl --request POST \
     --url https://demo.signalwire.com/api/video/room_tokens \
     --header 'Accept: application/json' \
     --header 'Authorization: Basic bmV2ZXIgZ29ubmEgZ2l2ZTp5b3UgdXA=' \
     --header 'Content-Type: application/json' \
     --data '
{
     "permissions": [
          "room.set_layout"
     ],
     "room_name": "example_room",
     "user_name": "example_user"
}
'

The Room.getLayouts() method

The Room object exposes methods for getting and setting layouts.

To know what layouts the video feed supports, use the Room.getLayouts() method.

roomObject.getLayouts().then(list=>console.log(list.layouts));

/*
    Outputs an array like:
  [
      "8x8",
      "2x1",
      "1x1",
      ...
  ]
*/

The Room.setLayout() method

To set the video call's layout to one of the layouts from the layout's list, use the Room.setLayout() method.

roomObject.setLayout({name: "10x10"}).then(output=>console.log(output));

If calling the .setLayout() method throws an insufficient permission error, then the access token you are using to instantiate the Room object wasn't given the room.set_layout permission. Learn more about the access tokens permission list in the docs.

Getting notified of layout changes

The active layout of a video conference can be changed by members who have that permission. To keep your UI in sync as the layout changes, use the layout.changed event of the Room object.

room.on("layout.changed", async (e) => {
     // e.layout has the properties of new layout
     onRoomUpdate({ layout: e.layout.name });
  });

The example in the previous part (Working with microphones, cameras and speakers) has been slightly modified below to list and change layouts:

import React, { useCallback, useState } from "react";
import Button from "react-bootstrap/Button";
import "bootstrap/dist/css/bootstrap.min.css";
import Video from "../components/Video";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import NavBar from "react-bootstrap/Navbar";

export default function InCall({ roomDetails }) {
  let [cameras, setCameras] = useState([]);
  let [microphones, setMicrophones] = useState([]);
  let [speakers, setSpeakers] = useState([]);
  let [layouts, setLayouts] = useState([]);
  let [curLayout, setCurLayout] = useState();

  let [room, setRoom] = useState({});

  let logEvent = useCallback((msg, title, variant) => {
    // a simple event logger that we will later replace
    // for a bootstrap toast
    console.log(msg, title, variant);
  }, []);

  let onRoomInit = useCallback(
    (room, layouts, cameras, microphones, speakers) => {
      setCameras(cameras);
      setMicrophones(microphones);
      setSpeakers(speakers);
      setRoom(room);
      setLayouts(layouts);
    },
    []
  );

  let onRoomUpdate = useCallback((updatedValues) => {
    if (updatedValues.cameras !== undefined) setCameras(updatedValues.cameras);
    if (updatedValues.speakers !== undefined)
      setSpeakers(updatedValues.speakers);
    if (updatedValues.microphones !== undefined)
      setMicrophones(updatedValues.microphones);
    if (updatedValues.layout !== undefined)
      setCurLayout(updatedValues.layout);
  }, []);

  function DeviceSelect({
    devices = [],
    onChange = (value) => {},
    deviceName = "device",
  }) {
    return (
      <select
        onChange={async (e) => {
          if (e.target.value !== "") onChange(e.target.value);
        }}
        defaultValue=""
      >
        <option value="" disabled hidden>
          Change {deviceName}
        </option>
        {devices.map((device) => (
          <option key={device.deviceId} value={device.deviceId}>
            {device.label}
          </option>
        ))}
      </select>
    );
  }

  return (
    <>
      <Container fluid>
        <Row className="mt-3">
          <Col
            style={{ backgroundColor: "black" }}
            className="justify-content-md-center"
          >
            {roomDetails.mod ? "Moderator" : "normal uwer"}
            <Video
              onRoomInit={onRoomInit}
              onRoomUpdate={onRoomUpdate}
              joinDetails={roomDetails}
              width={800}
              eventLogger={logEvent}
            />
          </Col>
        </Row>
      </Container>

      <NavBar fixed="bottom">
        <Container fluid className="justify-content-md-center">
          <Row>
          
           <Col md="auto">
              <select
                value={curLayout}
                onChange={(e) => room.setLayout({ name: e.target.value })}
              >
                <option value="" disabled hidden>
                  Select Layout
                </option>
                {layouts !== undefined &&
                  layouts.map((l) => (
                    <option key={l} value={l}>
                      {l}
                    </option>
                  ))}
              </select>
            </Col>

            <Col md="auto">
              <DeviceSelect
                onChange={(id) => {
                  room.updateCamera({ deviceId: id });
                }}
                deviceName="Camera"
                devices={cameras}
              />
            </Col>

            <Col md="auto">
              <DeviceSelect
                onChange={(id) => {
                  room.updateMicrophone({ deviceId: id });
                }}
                deviceName="Microphone"
                devices={microphones}
              />
            </Col>

            <Col md="auto">
              <DeviceSelect
                onChange={(id) => {
                  room.updateSpeaker({ deviceId: id });
                }}
                deviceName="Speaker"
                devices={speakers}
              />
            </Col>

            <Col md="auto">
              <Button
                onClick={async () => {
                  await room.leave();
                }}
                variant="danger"
              >
                Leave
              </Button>
            </Col>
          </Row>
        </Container>
      </NavBar>
    </>
  );
}
import React, {
    useEffect,
    useRef,
    useState
} from "react";
import axios from "axios";
import * as SignalWire from "@signalwire/js";

export default function Video({
    onRoomInit = () => {},
    width = 400,
    joinDetails: roomDetails = {
        room: "signalwire",
        name: "JohnDoe",
        mod: false,
    },
    eventLogger = (msg) => {
        console.log("Event:", msg);
    },
}) {
    let [setupDone, setSetupDone] = useState(false);
    let thisMemberId = useRef(null);

    useEffect(() => {
        if (setupDone) return;
        setup_room();
        async function setup_room() {
            setSetupDone(true);
            let token, room;
            try {
                token = await axios.post("/get_token", {
                    user_name: roomDetails.name,
                    room_name: roomDetails.room,
                    mod: roomDetails.mod,
                });
                console.log(token.data);
                token = token.data.token;

                try {
                    try {
                        room = await SignalWire.Video.createRoomObject({
                            token,
                            rootElementId: "stream",
                            video: true,
                        });
                    } catch (e) {
                        console.log(e);
                    }
                    room.on("room.joined", async (e) => {
                        thisMemberId.current = e.member_id;
                        eventLogger("You have joined the room.");
                    });
                    room.on("room.updated", async (e) => {
                        eventLogger("Room has been updated");
                    });
                    room.on("member.joined", async (e) => {
                        eventLogger(e.member.name + " has joined the room.");
                    });
                    room.on("layout.changed", async (e) => {
                        // To change the selected layout
                                    onRoomUpdate({ layout: e.layout.name });
                            });
                    room.on("member.left", async (e) => {
                        let memberList = await room.getMembers();
                        let member = memberList.current.filter((m) => m.id === e.member.id);
                        if (member.length === 0) {
                            return;
                        }
                        eventLogger(member[0]?.name + " has left the room.");
                    });

                    await room.join();

                    let layouts = (await room.getLayouts()).layouts;
                    let cameras =
                        await SignalWire.WebRTC.getCameraDevicesWithPermissions();
                    let microphones =
                        await SignalWire.WebRTC.getMicrophoneDevicesWithPermissions();
                    let speakers =
                        await SignalWire.WebRTC.getSpeakerDevicesWithPermissions();

                    onRoomInit(room, layouts, cameras, microphones, speakers);

                    let camChangeWatcher = await SignalWire.WebRTC.createDeviceWatcher({
                        targets: ["camera"],
                    });
                    camChangeWatcher.on("changed", (changes) => {
                        eventLogger("The list of camera devices has changed");
                        onRoomUpdate({
                            cameras: changes.devices
                        });
                    });
                    let micChangeWatcher = await SignalWire.WebRTC.createDeviceWatcher({
                        targets: ["microphone"],
                    });
                    micChangeWatcher.on("changed", (changes) => {
                        eventLogger("The list of microphone devices has changed");
                        onRoomUpdate({
                            microphones: changes.devices
                        });
                    });
                    let speakerChangeWatcher =
                        await SignalWire.WebRTC.createDeviceWatcher({
                            targets: ["speaker"],
                        });
                    speakerChangeWatcher.on("changed", (changes) => {
                        eventLogger("The list of speakers has changed");
                        onRoomUpdate({
                            speakers: changes.devices
                        });
                    })

                } catch (error) {
                    console.error("Something went wrong", error);
                }
            } catch (e) {
                console.log(e);
                alert("Error encountered. Please try again.");
            }
        }
    }, [roomDetails, eventLogger, onRoomInit, onRoomUpdate, setupDone]);
    return (<div id = "stream" style = {{width}}/>);
}

Did this page help you?