Skip to content

Composite / Web

Composite recording (also known as web recording or headless browser recording) combines all participants into a single video file with a customizable layout. This is the easiest way to get started with call recording—you get a ready-to-share file with no post-processing required.

Quickstart

// Start composite recording
// Triggers webhook event: call.recording_started
await call.startRecording("composite");

// Stop composite recording
// Triggers webhook event: call.recording_stopped
// When processing completes: call.recording_ready (contains recording URL)
await call.stopRecording("composite");

// List all recordings for this call
const response = await call.listRecordings();

// Download using the URL from response.recordings or the call.recording_ready webhook payload
// response.recordings[0].url

How it works

When you start a composite recording, a hidden participant joins the call running a headless browser. This browser renders the video layout in real-time (grid, spotlight, or single participant) and the display output is captured and encoded into a single MP4 file. The recording is processed server-side and stored on AWS S3 (Stream-managed by default, or your own bucket).

When to use composite recording

Advantages:

  • Easy to get started—single file output ready for immediate playback
  • Customizable layouts and branding (logos, colors, participant labels)
  • No post-processing infrastructure needed
  • Audio-only option available for lighter storage

Limitations:

  • Higher cost compared to raw or individual recording (due to real-time rendering)
  • Layout is fixed at recording time—you cannot reframe or re-layout after the fact
  • Resolution and orientation are determined when recording starts and do not adapt during the call (e.g., if a participant switches from landscape to portrait, the recording won't adjust)

For use cases requiring post-production flexibility or per-participant tracks, consider individual recording or raw recording.

Events

These events are sent to users connected to the call and your webhook/SQS, each event contains a new recording_type field that refers to the type of recording (composite in this case):

User Permissions

The following permissions are checked when users interact with the call recording API.

  • StartRecording required to start the recording
  • StopRecording required to stop the recording

For listing and deleting recordings, see Recording Management.

Enabling / Disabling call recording

Recording can be configured from the Dashboard (see call type screen) or directly via the API. It is also possible to change the recording settings for a call and override the default settings coming from the its call type.

// Disable on call level
call.update({
  settings_override: {
    recording: {
      mode: "disabled",
    },
  },
});

// Disable on call type level
client.video.updateCallType({
  name: "<call type name>",
  settings: {
    recording: {
      mode: "disabled",
    },
  },
});

// Automatically record calls
client.video.updateCallType({
  name: "<call type name>",
  settings: {
    recording: {
      mode: "auto-on",
      quality: "720p",
    },
  },
});

// Enable
call.update({
  settings_override: {
    recording: {
      mode: "available",
    },
  },
});

// Other settings
call.update({
  settings_override: {
    recording: {
      mode: "available",
      audio_only: false,
      quality: "1080p",
    },
  },
});

Audio only recording

You can configure your calls to only record the audio tracks and exclude the video. This produces a single MP3 file with all participants mixed together. You can enable this from the dashboard (Call Types section) or set it for individual calls.

// Enable
call.update({
  settings_override: {
    recording: {
      mode: "available",
      audio_only: true,
    },
  },
});

Recording layouts

Recording can be customized in several ways:

  • You can pick one of the built-in layouts and pass some options to it
  • You can further customize the style of the call by providing your own CSS file
  • You can use your own recording application

There are three available layouts you can use for your calls: "single-participant", "grid" and "spotlight"

Single Participant

This layout shows only one participant video at a time, other video tracks are hidden.

Layout Single Participant

The visible video is selected based on this priority:

  • Participant is pinned
  • Participant is screen-sharing
  • Participant is the dominant speaker
  • Participant has a video track

Grid

This layout shows a configurable number of tracks in an equally sized grid.

Layout Grid

Spotlight

This layout shows a video in a spotlight and the rest of the participants in a separate list or grid.

Layout Spotlight

Layout options

Each layout has a number of options that you can configure. Here is an example:

Layout Custom Options
const layoutOptions = {
  "logo.image_url":
    "https://theme.zdassets.com/theme_assets/9442057/efc3820e436f9150bc8cf34267fff4df052a1f9c.png",
  "logo.horizontal_position": "center",
  "title.text": "Building Stream Video Q&A",
  "title.horizontal_position": "center",
  "title.color": "black",
  "participant_label.border_radius": "0px",
  "participant.border_radius": "0px",
  "layout.spotlight.participants_bar_position": "top",
  "layout.background_color": "#f2f2f2",
  "participant.placeholder_background_color": "#1f1f1f",
  "layout.single-participant.padding_inline": "20%",
  "participant_label.background_color": "transparent",
};

client.video.updateCallType({
  name: callTypeName,
  settings: {
    recording: {
      mode: "available",
      audio_only: false,
      quality: "1080p",
      layout: {
        name: "spotlight",
        options: layoutOptions,
      },
    },
  },
});

Options are passed as a flat key/value map under layout.options. The tables below list every option the default layout app supports.

Common options (all layouts)

These options apply to the single-participant, grid, and spotlight layouts.

Option Type Default Allowed Values Description
video.background_color color Background color of the video element
video.scale_mode string browser default fit, fill How camera video fills its box when the aspect ratio differs. fill crops to cover, fit letterboxes to contain
video.screenshare_scale_mode string fit fit, fill Same as video.scale_mode but for screen share tracks
logo.image_url string URL of a logo image to overlay on the layout
logo.width string initial CSS size Logo width
logo.height string 40px CSS size Logo height
logo.horizontal_position string right center, left, right Horizontal position of the logo
logo.vertical_position string bottom center, top, bottom Vertical position of the logo
logo.margin_inline string .875rem CSS size Horizontal margin around the logo
logo.margin_block string .875rem CSS size Vertical margin around the logo
title.text string Title text to overlay on the layout
title.font_size string 30px CSS size Title font size
title.color color white Title text color
title.horizontal_position string left center, left, right Horizontal position of the title
title.vertical_position string top center, top, bottom Vertical position of the title
title.margin_inline string/number .875rem CSS size Horizontal margin around the title
title.margin_block string/number .875rem CSS size Vertical margin around the title
participant.aspect_ratio string e.g. "16 / 9", "4 / 3", "1 / 1", "9 / 16" Aspect ratio of each participant tile (ignored while screen sharing)
participant.border_radius string/number CSS size Corner radius of the participant tile
participant.outline_color color #005fff Outline color applied to the currently speaking participant
participant.outline_width string 2px CSS size Outline width applied to the currently speaking participant
participant.placeholder_background_color color Background color of the placeholder shown when a participant has no video
participant.filter object See Filtering participants Optional filter to determine which participants are displayed
participant_label.display boolean true Show the participant label
participant_label.text_color color Text color of the participant label
participant_label.background_color color Background color of the participant label
participant_label.border_width string 0 CSS size Border width of the participant label
participant_label.border_color color rgba(0,0,0,0) Border color of the participant label
participant_label.border_radius string/number CSS size Corner radius of the participant label
participant_label.horizontal_position string left center, left, right Horizontal position of the participant label
participant_label.vertical_position string bottom center, top, bottom Vertical position of the participant label
participant_label.margin_inline string 0 CSS size Horizontal margin around the participant label
participant_label.margin_block string 0 CSS size Vertical margin around the participant label
layout.background_color color Background color of the layout
layout.background_image string CSS background-image Background image of the layout, e.g. url(https://...)
layout.background_size string CSS background-size Background image sizing, e.g. cover
layout.background_position string CSS background-position Background image position
layout.background_repeat string CSS background-repeat Background image repeat behavior
layout.forceMirrorParticipants boolean true, false Forces participant camera videos to be mirrored or unmirrored. When omitted, the default mirroring behavior applies
custom_actions array See Custom actions Optional array of custom actions executed when a defined condition is met
debug.show_timestamp boolean false true, false Renders a high-precision timestamp overlay over the video, useful for debugging frame timing

Single Participant

In addition to the common options, the single-participant layout supports:

Option Type Default Allowed Values Description
layout.single-participant.mode string default default, shuffle When set to shuffle, periodically rotates which participant is shown
layout.single-participant.shuffle_delay number Delay (in seconds) between shuffles when mode is shuffle
layout.single-participant.padding_inline string 0px CSS size Horizontal padding around the spotlighted participant
layout.single-participant.padding_block string/number 0px CSS size Vertical padding around the spotlighted participant
layout.single-participant.presenter_visible boolean true true, false Show the presenter's camera video during screen sharing

Spotlight

In addition to the common options, the spotlight layout supports:

Option Type Default Allowed Values Description
layout.spotlight.participants_bar_position string bottom top, bottom, left, right Position of the participants bar relative to the spotlight
layout.spotlight.participants_bar_limit number/string dynamic dynamic or a number Max number of participants in the bar. dynamic fits as many as the available space

Grid

In addition to the common options, the grid layout supports:

Option Type Default Allowed Values Description
layout.grid.page_size number 20 Maximum number of participant tiles shown per page
layout.grid.expand_tiles boolean false true, false When enabled, participant tiles expand to fill the available space while maintaining a 16:9 aspect ratio
Info:

Screen share layout is configured separately from layout.options (via the call type's screen share layout setting). The supported screen share layouts are single-participant, spotlight, and dominant-speaker.

Recording resolution and portrait mode

Calls can be recorded in different resolutions and modes (landscape and portrait). On the dashboard, you can configure the default settings for all calls of a specific call type.

Recording Resolution And Orientation

While this can be configured from the dashboard, you can also set it for individual calls:

client.video.updateCallType({
  name: callTypeName,
  settings: {
    recording: {
      mode: "available",
      quality: "portrait-1080x1920",
    },
  },
});

Filtering participants

The participant.filter option allows you to choose which participants are visible in the recording. Its value is a special filter object.

The following properties are allowed to be used in the filter object:

Property Type Description
userId string Participant's user id
isSpeaking boolean Indicates wheather the participant is currently speaking
isDominantSpeaker boolean Indicates wheather the participant is a dominant speaker (only one participant can be a dominant speaker at the time)
name string Participant's user name
roles string[] List of participant's roles in the current call
isPinned boolean Indicates wheather the participant is pinned
hasVideo boolean Indicates whether the participant has video
hasAudio boolean Indicates whether the participant has audio
hasScreenShare boolean Indicates whether the participant has screen share video

For example, to include only pinned participants in the recording, you can provide the following filter:

{
  "participant.filter": {
    "isPinned": true
  }
}

If you want to filter participants based on their role, keep in mind that a participant can have more than one role within a call. Because the roles property is an array, you must use the $contains operator to build your filter. For example, this filter will only match participants with the role "admin":

{
  "participant.filter": {
    "roles": { "$contains": "admin" }
  }
}

When recording livestreams, including only participants with video is an easy way to exclude viewers from the recording:

{
  "participant.filter": {
    "hasVideo": true
  }
}

Other operators you can use are $neq ("not equal to") and $in ("equal to one of the listed values"). For example, this filter will only match participants with one of the three names:

{
  "participant.filter": {
    "name": { "$in": ["Moe", "Larry", "Curly"] }
  }
}

You can also use the $eq ("equals") operator, but its effect is the same as not using any operator at all, as in the first example.

You can combine multiple conditions using the $and, $or, and $not operators:

// Hide participants with the role "guest", unless they have been pinned:
{
  "participant.filter": {
    "$or": [
      { "$not": { "roles": { "$contains": "guest" } } },
      { "isPinned": true }
    ]
  }
}

// Show participants that either have video or screen share
{
  "participant.filter": {
    "$or": [
      { "hasVideo": true },
      { "hasScreenShare": true }
    ]
  }
}

Custom actions

Custom actions let you dynamically adjust the recording setup during the runtime based on the conditions you define under options.custom_actions configuration option. The function that evaluates the conditions is the same as in Filtering participants (participant.filter option) and allows using the same operators.

Available actions

layout_override

Override the current layout when a condition matches. The first matching layout_override action wins (checked top-to-bottom).

Property Type Allowed Values Description
action_type layout_override
layout string [grid spotlight single-participant dominant-speaker] One of the supported layouts
ignore_screenshare boolean When true, keeps the override even during screen sharing; otherwise screenshare_layout takes precedence
condition object

options_override

Override any supported options.* values when a condition matches. All matching options_override actions are merged in order; later actions overwrite earlier ones for the same option keys.

Property Type Allowed Values Description
action_type options_override
options object Refer to Single Participant, Spotlight and Grid A partial of the supported options keys (anything you normally set under options.*), custom_actions are omitted
condition object

Target values & supported operators

You can target these values in conditions:

Property Type Description
participant_count number Number of participants in the call
pinned_participant_count number Number of pinned participants in the call (evaluated client-side)

Logical operators:

  • $and, $or, $not

Scalar operators:

  • $eq, $neq, $in, $gt, $gte, $lt, $lte
Info:

The number of target values is currently limited but if you need other properties available feel free to reach out to our support.

Examples

Switch to a dominant-spearker layout when pinned_participant_count is greater or equal to one:

{
  // other options
  "custom_actions": [
    {
      "action_type": "layout_override",
      "condition": {
        "pinned_participant_count": { "$gte": 1 }
      },
      "layout": "dominant-speaker"
    }
  ]
}

Apply different background color to the recording setup when second participant joins the call:

{
  // other options
  "custom_actions": [
    {
      "action_type": "options_override",
      "condition": {
        "participant_count": { "$gt": 1 }
      },
      "options": {
        "layout.background_color": "hotpink"
      }
    }
  ]
}

Custom recording styling using external CSS

You can customize how recorded calls look by providing an external CSS file. The CSS file needs to be publicly available and ideally hosted on a CDN to ensure the best performance. The best way to find the right CSS setup is by running the layout app directly. The application is publicly available on Github here and contains instructions on how to be used.

client.video.updateCallType({
  name: callTypeName,
  settings: {
    recording: {
      mode: "available",
      audio_only: false,
      quality: "1080p",
      layout: {
        name: "spotlight",
        external_css_url: "https://path/to/custom.css",
      },
    },
  },
});

Advanced - record calls using a custom web application

If needed, you can use your own custom application to record a call. This is the most flexible and complex approach to record calls, make sure to reach out to our customer support before going with this approach.

The layout app used to record calls is available on GitHub and is a good starting point. The repository also includes information on how to build your own.

client.video.updateCallType({
  name: callTypeName,
  settings: {
    recording: {
      mode: "available",
      audio_only: false,
      quality: "1080p",
      layout: {
        name: "custom",
        external_app_url: "https://path/to/layout/app",
      },
    },
  },
});

Client-side recording

Unfortunately, there is no direct support for client-side recording at the moment. Call recording at the moment is done by Stream server-side. If client-side recording is important for you please make sure to follow the conversation here.