> For a complete index of all SignalWire documentation pages, fetch https://signalwire.com/docs/llms.txt

# Layouts & Participant Views

For multi-party video rooms, the SignalWire platform composes every
participant's camera into a single **mixed video stream** —
[`call.remoteStream$`][`remoteStream$`] emits that stream. A
**layout** is the rule the server uses to composite it: grid,
presenter + thumbnails, picture-in-picture, etc. Each room has a
list of layouts the server allows; clients pick one, optionally pin
who goes in which slot, and read back where everyone ended up.

The server composes the video. The client picks the composition
and draws overlays (name tags, mute icons, click targets) on top
using percentage-based layer coordinates.

## Pick a layout

Wire a picker to [`layouts$`] (available options), [`layout$`]
(current selection), and [`setLayout()`] (mutator):

```js
call.layouts$.subscribe((names) => {
  layoutPicker.innerHTML = names
    .map((n) => `<option value="${n}">${n}</option>`)
    .join("");
});

call.layout$.subscribe((current) => {
  layoutPicker.value = current ?? "";
});

layoutPicker.onchange = () => call.setLayout(layoutPicker.value, {});
```

The available layout names are server-defined — they depend on the
room's configuration. `setLayout` rejects with `InvalidParams` if you
pass a name that isn't in `layouts$`, so either bind from the picker
options (as above) or validate up front.

The empty `{}` second argument means "let the server place
participants automatically." To pin specific members into specific
slots:

```js
await call.setLayout("presenter", {
  [presenterId]: "reserved-0",  // big slot
  [guestId]:     "reserved-1",  // sidebar
});
```

Slot names (`reserved-0`, `reserved-1`, `auto`, `standard-0`, …) are
defined per-layout — see [`VideoPosition`]. Members not in the map
are auto-placed. The local user can request their own position too
with [`call.self.setPosition()`][`Participant.setPosition()`], gated
by `capabilities.self.position`.

## Render the layout

Attach the mixed stream to a single `<video>` element:

```html
<video id="room" autoplay playsinline></video>
```

```js
call.remoteStream$.subscribe((s) => roomVideo.srcObject = s);
```

The stream already contains every participant arranged by the
current layout — don't render per-participant `<video>` tags.

## Draw overlays

Overlays (name tags, mute icons, click hotspots, speaking borders)
go on top of the single video. [`layoutLayers$`] emits
per-participant boxes with **percentage** coordinates (0–100)
relative to the room canvas, so overlays scale with the video
element regardless of resolution.

```js
call.layoutLayers$.subscribe((layers) => {
  for (const layer of layers) {
    overlay(layer.member_id).style.cssText = `
      left:   ${layer.x}%;
      top:    ${layer.y}%;
      width:  ${layer.width}%;
      height: ${layer.height}%;
    `;
  }
});
```

See [`LayoutLayer`] for the full layer shape (z-index, visibility,
reservation slot, etc.).

For per-tile UI, each `Participant` has its own
[`position$`][`Participant.position$`] scoped to that member —
simpler than filtering `layoutLayers$` on every emission.

## Re-shuffles

[`layout$`] and [`layoutLayers$`] re-emit whenever the server
re-composites: `setLayout` calls, members joining or leaving under
auto-layout, the server promoting a raised hand. Subscriptions stay
live; overlays follow automatically.

## Capability gating

| Action                        | Capability                      |
| ----------------------------- | ------------------------------- |
| Pick a different layout       | [`SelfCapabilities.setLayout$`] |
| Set your own position         | `capabilities.self.position`    |
| Set another member's position | `capabilities.member.position`  |

Hide the picker / position controls when the capability is false.
See [Capabilities](/docs/browser-sdk/v4/guides/capabilities).

## Reference

* [`layouts$`] · [`layout$`] · [`layoutLayers$`] — what to subscribe to
* [`setLayout()`] — switch the composition / pin slots
* [`Participant.position$`] · [`Participant.setPosition()`] — per-member position
* [`LayoutLayer`] · [`VideoPosition`] — data shapes
* [`SelfCapabilities.setLayout$`] — capability gate

[`remoteStream$`]: /docs/browser-sdk/v4/reference/webrtc-call/remote-stream$

[`layouts$`]: /docs/browser-sdk/v4/reference/webrtc-call/layouts$

[`layout$`]: /docs/browser-sdk/v4/reference/webrtc-call/layout$

[`layoutLayers$`]: /docs/browser-sdk/v4/reference/webrtc-call/layout-layers$

[`setLayout()`]: /docs/browser-sdk/v4/reference/webrtc-call/set-layout

[`Participant.position$`]: /docs/browser-sdk/v4/reference/participant/position$

[`Participant.setPosition()`]: /docs/browser-sdk/v4/reference/participant/set-position

[`LayoutLayer`]: /docs/browser-sdk/v4/reference/interfaces/layout-layer

[`VideoPosition`]: /docs/browser-sdk/v4/reference/type-aliases/video-position

[`SelfCapabilities.setLayout$`]: /docs/browser-sdk/v4/reference/self-capabilities/set-layout$