External VAST Ads

Render your own VAST tag through the StreamLayer Element with the externalAdData prop, in side-by-side, sidebar, or L-bar format.

Overview

By default, ad units arrive from StreamLayer Studio through the event feed. The externalAdData
prop lets your app drive the ad itself: you hand the SDK a VAST tag URL and the format you want,
and the SDK builds an ad unit from it and renders it through the same pipeline as a Studio-scheduled
ad — including VAST impression, quartile, and click-through tracking.

Use this when the creative is selected by your own ad stack rather than by StreamLayer Studio.

Requires @streamlayer/react 2.0.0 or later.


Quick Start

import { useMemo, useRef } from 'react'
import { StreamLayerProvider } from '@streamlayer/react'
import { StreamLayerSDKAdvertisement } from '@streamlayer/react/advertisement'
import '@streamlayer/react/style.css'

export const Player = () => {
  const videoContainerRef = useRef<HTMLDivElement>(null)

  // Memoize the object — the SDK keys the injected ad unit on its identity.
  const externalAdData = useMemo(
    () => ({
      vastUrl: 'https://example.com/vast.xml',
      uiType: 'side-by-side' as const,
    }),
    []
  )

  return (
    <div id="player-root" style={{ position: 'relative' }}>
      <div ref={videoContainerRef}>
        <video src="https://example.com/stream.m3u8" autoPlay controls />
      </div>

      <StreamLayerProvider sdkKey="your-sdk-key" event="your-event-id" containerId="player-root">
        <StreamLayerSDKAdvertisement
          sideBySide
          videoRef={videoContainerRef}
          externalAdData={externalAdData}
        />
      </StreamLayerProvider>
    </div>
  )
}

The ad unit appears as soon as the VAST tag resolves, and is removed when externalAdData changes
to a different tag or the component unmounts.


ExternalAdInput

type ExternalAdUiType = 'side-by-side' | 'sidebar' | 'l-bar'

type ExternalAdInput = {
  /** VAST tag URL — raw VAST/VMAP, or a Google Ad Manager ad-tag URL. */
  vastUrl: string
  /** Which format the ad unit renders in. */
  uiType: ExternalAdUiType
  /** Background image URL for landscape layouts. */
  landscapeBackground?: string
  /** Background image URL for portrait layouts. */
  portraitBackground?: string
}
FieldTypeDescription
vastUrlstringRequired. VAST tag URL. Raw VAST/VMAP is fetched directly; a pubads.g.doubleclick.net tag is resolved through Google PAL.
uiType'side-by-side' | 'sidebar' | 'l-bar'Required. Where the ad unit renders. See the table below.
landscapeBackgroundstringOptional. Background image URL behind the side-by-side ad unit in landscape layouts. Defaults to the StreamLayer background.
portraitBackgroundstringOptional. Background image URL behind the side-by-side ad unit in portrait layouts. Defaults to the StreamLayer background.

Formats

uiTypeRenders asCreative used
'side-by-side'Double box beside the host videoVAST linear creative, played in the SDK's own player
'sidebar'Ordinary sidebar slotVAST linear creative as the sidebar media
'l-bar'Sidebar plus bottom banner stripLinear creative in the sidebar; the VAST companion ad as the strip

An 'l-bar' tag whose VAST carries no companion ad degrades to a plain sidebar — no empty strip is
reserved.

The sidebar video autoplays muted. Title and description come from the VAST <AdTitle> and
<Description> elements.


Slot Placement

Explicit host intent wins: an ad injected with uiType renders where the uiType says, regardless
of the VAST streamlayer extension. Ads that arrive from the event
feed follow the extension's root element first, then the format configured in StreamLayer Studio;
side-by-side is used only when an extension is present but neither names a format.

Because an external ad normally belongs to the side-by-side slot, useStreamLayerUI() alone can't
tell you whether to keep your sidebar and banner slots mounted for one. The
useExternalAdPlacement hook resolves this:

import { useStreamLayerUI } from '@streamlayer/react'
import {
  StreamLayerSDKAdvertisement,
  useExternalAdPlacement,
} from '@streamlayer/react/advertisement'

const Layout = () => {
  const uiState = useStreamLayerUI()
  const { externalSidebar, externalLbar } = useExternalAdPlacement(uiState.promotionExternalAd)

  return (
    <>
      <aside style={{ width: externalSidebar || uiState.promotionSidebar ? 320 : 0 }}>
        <StreamLayerSDKAdvertisement sidebar="right" externalAdData={externalAdData} />
      </aside>

      {externalLbar && (
        <div className="banner">
          <StreamLayerSDKAdvertisement banner="bottom" externalAdData={externalAdData} />
        </div>
      )}
    </>
  )
}
Return valueMeaning
externalSidebarThe active external ad resolves to the sidebar or L-bar format — keep the sidebar slot sized.
externalLbarThe active external ad resolves to L-bar and its VAST carries a companion — keep the banner slot sized.

Both flip only once the VAST has been parsed, so the sidebar never opens on an empty creative.


VAST streamlayer Extension

Standard VAST carries the media, title, description, and click-through, but not the branding a
StreamLayer ad unit needs around it. Your ad server supplies that through a StreamLayer
<Extension> in the VAST response. The SDK reads it for every external ad — injected with
externalAdData or scheduled in StreamLayer Studio.

Where the SDK looks

  • The SDK takes the first <Extension> whose type attribute starts with streamlayer
    (for example streamlayer or streamlayer/double-box/v1). Other extensions are ignored.
  • The first child element of that extension is its root. The fields below are read from the
    root's children.
  • The root's element name selects the format. DoubleBox is the only name defined today; any other
    name is still read for the fields below but does not change the format.

Fields

ElementAttributesUsed for
AdName—Internal campaign name. Parsed but not rendered.
Backgrounds/Background/Urlorientation="landscape"Background image of the ad unit in landscape layouts (TV and desktop web).
Backgrounds/Background/Urlorientation="portrait"Background image of the ad unit in portrait layouts (mobile web).
Logos/Logo/Urlrole="header"Sponsor logo of the ad unit.
CTA/Label—CTA button label for the sidebar and L-bar formats.
CTA/Type—Reserved. Parsed but not used.

width, height, and type attributes on Background and Logo describe the asset; the SDK does
not use them to pick between assets. When several Background elements share an orientation, the
first one wins.

Example

<Extensions>
  <Extension type="streamlayer/double-box/v1">
    <DoubleBox>
      <AdName><![CDATA[Spring 2026 — Double Box]]></AdName>
      <Backgrounds>
        <Background orientation="landscape" width="2560" height="1440" type="image/png">
          <Url><![CDATA[https://cdn.example.com/creatives/12345/bg-landscape.png]]></Url>
        </Background>
        <Background orientation="portrait" width="1000" height="1500" type="image/png">
          <Url><![CDATA[https://cdn.example.com/creatives/12345/bg-portrait.png]]></Url>
        </Background>
      </Backgrounds>
      <Logos>
        <Logo role="header" width="300" height="75" type="image/png">
          <Url><![CDATA[https://cdn.example.com/creatives/12345/logo.png]]></Url>
        </Logo>
      </Logos>
      <CTA>
        <Label><![CDATA[Shop Now]]></Label>
      </CTA>
    </DoubleBox>
  </Extension>
</Extensions>

Place <Extensions> inside the <InLine> ad, next to <Creatives>. See
Double Box Creative Specs for recommended asset sizes
and a complete sample VAST response.

Precedence and fallbacks

FieldExtension presentExtension missing the field
BackgroundThe extension image replaces the background configured in StreamLayer Studio.Studio background, otherwise the StreamLayer default background.
Sponsor logoThe extension logo replaces the Studio sponsor logo. In the sidebar and L-bar formats, a 300×75 VAST companion logo is drawn in preference to it.Studio logo, or the VAST companion logo in the sidebar and L-bar formats.
CTA labelThe extension label replaces the Studio label (sidebar and L-bar only).Studio label, otherwise LEARN MORE.

An ad injected with externalAdData has no Studio configuration behind it, so the extension is
its only source of background and branding
. ExternalAdInput has no field to override them from
the Host App — change the VAST response to change them.

Note: The extension is read only for external ads. Pause ads set through
pauseAdExternalUrls ignore it. See Exposed Pause Ad.


Rules and Limits

  • Memoize externalAdData. The SDK derives a stable id from vastUrl + uiType, but a fresh
    object literal on every render still churns the effect that injects and removes the ad unit. Use
    useMemo, or a module-level constant when the tag is fully static.
  • Backgrounds are read once. landscapeBackground and portraitBackground are applied when the
    ad unit is injected and are not part of its id. Changing only the backgrounds does not update an ad
    unit that is already injected; change vastUrl or uiType, or remount, to apply new ones.
  • One injected ad at a time. The advertisement store holds a single ad unit, so only one
    externalAdData instance per page is supported. Injecting replaces any active ad unit, and a
    Studio-scheduled ad replaces an injected one.
  • externalAdData implies external-ad mode. You do not also need to pass the boolean
    externalAd prop for it to render.
  • Don't double up slots. A slot marked with the boolean externalAd prop is the programmatic
    GAM/PAL slot; it stands down for sidebar and L-bar placements so the plain slot renders the ad
    once. This behavior changed in 2.0.0.
  • Mute the host video. Pass a videoPlayerController so the host stream is muted while the ad's
    audio plays. See Video Controller.

Related