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.

OptionTypeDefaultAllowed ValuesDescription
video.background_colorcolorBackground color of the video element
video.scale_modestringbrowser defaultfit, fillHow camera video fills its box when the aspect ratio differs. fill crops to cover, fit letterboxes to contain
video.screenshare_scale_modestringfitfit, fillSame as video.scale_mode but for screen share tracks
logo.image_urlstringURL of a logo image to overlay on the layout
logo.widthstringinitialCSS sizeLogo width
logo.heightstring40pxCSS sizeLogo height
logo.horizontal_positionstringrightcenter, left, rightHorizontal position of the logo
logo.vertical_positionstringbottomcenter, top, bottomVertical position of the logo
logo.margin_inlinestring.875remCSS sizeHorizontal margin around the logo
logo.margin_blockstring.875remCSS sizeVertical margin around the logo
title.textstringTitle text to overlay on the layout
title.font_sizestring30pxCSS sizeTitle font size
title.colorcolorwhiteTitle text color
title.horizontal_positionstringleftcenter, left, rightHorizontal position of the title
title.vertical_positionstringtopcenter, top, bottomVertical position of the title
title.margin_inlinestring/number.875remCSS sizeHorizontal margin around the title
title.margin_blockstring/number.875remCSS sizeVertical margin around the title
participant.aspect_ratiostringe.g. "16 / 9", "4 / 3", "1 / 1", "9 / 16"Aspect ratio of each participant tile (ignored while screen sharing)
participant.border_radiusstring/numberCSS sizeCorner radius of the participant tile
participant.outline_colorcolor#005fffOutline color applied to the currently speaking participant
participant.outline_widthstring2pxCSS sizeOutline width applied to the currently speaking participant
participant.placeholder_background_colorcolorBackground color of the placeholder shown when a participant has no video
participant.filterobjectSee Filtering participantsOptional filter to determine which participants are displayed
participant_label.displaybooleantrueShow the participant label
participant_label.text_colorcolorText color of the participant label
participant_label.background_colorcolorBackground color of the participant label
participant_label.border_widthstring0CSS sizeBorder width of the participant label
participant_label.border_colorcolorrgba(0,0,0,0)Border color of the participant label
participant_label.border_radiusstring/numberCSS sizeCorner radius of the participant label
participant_label.horizontal_positionstringleftcenter, left, rightHorizontal position of the participant label
participant_label.vertical_positionstringbottomcenter, top, bottomVertical position of the participant label
participant_label.margin_inlinestring0CSS sizeHorizontal margin around the participant label
participant_label.margin_blockstring0CSS sizeVertical margin around the participant label
layout.background_colorcolorBackground color of the layout
layout.background_imagestringCSS background-imageBackground image of the layout, e.g. url(https://...)
layout.background_sizestringCSS background-sizeBackground image sizing, e.g. cover
layout.background_positionstringCSS background-positionBackground image position
layout.background_repeatstringCSS background-repeatBackground image repeat behavior
layout.forceMirrorParticipantsbooleantrue, falseForces participant camera videos to be mirrored or unmirrored. When omitted, the default mirroring behavior applies
custom_actionsarraySee Custom actionsOptional array of custom actions executed when a defined condition is met
debug.show_timestampbooleanfalsetrue, falseRenders 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:

OptionTypeDefaultAllowed ValuesDescription
layout.single-participant.modestringdefaultdefault, shuffleWhen set to shuffle, periodically rotates which participant is shown
layout.single-participant.shuffle_delaynumberDelay (in seconds) between shuffles when mode is shuffle
layout.single-participant.padding_inlinestring0pxCSS sizeHorizontal padding around the spotlighted participant
layout.single-participant.padding_blockstring/number0pxCSS sizeVertical padding around the spotlighted participant
layout.single-participant.presenter_visiblebooleantruetrue, falseShow the presenter's camera video during screen sharing

Spotlight

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

OptionTypeDefaultAllowed ValuesDescription
layout.spotlight.participants_bar_positionstringbottomtop, bottom, left, rightPosition of the participants bar relative to the spotlight
layout.spotlight.participants_bar_limitnumber/stringdynamicdynamic or a numberMax 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:

OptionTypeDefaultAllowed ValuesDescription
layout.grid.page_sizenumber20Maximum number of participant tiles shown per page
layout.grid.expand_tilesbooleanfalsetrue, falseWhen enabled, participant tiles expand to fill the available space while maintaining a 16:9 aspect ratio

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:

PropertyTypeDescription
userIdstringParticipant's user id
isSpeakingbooleanIndicates wheather the participant is currently speaking
isDominantSpeakerbooleanIndicates wheather the participant is a dominant speaker (only one participant can be a dominant speaker at the time)
namestringParticipant's user name
rolesstring[]List of participant's roles in the current call
isPinnedbooleanIndicates wheather the participant is pinned
hasVideobooleanIndicates whether the participant has video
hasAudiobooleanIndicates whether the participant has audio
hasScreenSharebooleanIndicates 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).

PropertyTypeAllowed ValuesDescription
action_typelayout_override
layoutstring[grid spotlight single-participant dominant-speaker]One of the supported layouts
ignore_screensharebooleanWhen true, keeps the override even during screen sharing; otherwise screenshare_layout takes precedence
conditionobject

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.

PropertyTypeAllowed ValuesDescription
action_typeoptions_override
optionsobjectRefer to Single Participant, Spotlight and GridA partial of the supported options keys (anything you normally set under options.*), custom_actions are omitted
conditionobject

Target values & supported operators

You can target these values in conditions:

PropertyTypeDescription
participant_countnumberNumber of participants in the call
pinned_participant_countnumberNumber of pinned participants in the call (evaluated client-side)

Logical operators:

  • $and, $or, $not

Scalar operators:

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

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.