Server-Guided Ad Insertion with Web Player

Server-Guided Ad Insertion (SGAI) lets the Bitmovin Web Player play ads that your server signals directly in the HLS manifest. This guide shows you how to enable and control it on Web. For a product-agnostic overview — what SGAI is, HLS Interstitials, and how manifests signal ads — see the SGAI overview.

On Web, HLS Interstitials power SGAI: the player reads interstitial tags from the manifest, loads the ad assets they point to, plays each ad at the position its tag marks, and fires the standard advertising events. You do not schedule ad breaks from the client (unlike client-side advertising).

Prerequisites

SGAI / HLS Interstitials handling is provided by the SGAI module, which depends on the Advertising Core module. Viewability tracking additionally needs the Style module. How you make these available depends on which player build you use.

Bundled player

The bundled (all-in-one) bitmovinplayer.js build already includes every module. You don't need to load or register anything extra — just enable the enable_sgai_handling tweak (see Basic setup).

<script src="//cdn.bitmovin.com/player/web/8/bitmovinplayer.js"></script>

Modular player

With the modular player you must load and register each required module before creating the player:

<script src="//cdn.bitmovin.com/player/web/8/bitmovinplayer.js"></script>
<!-- Advertising Core — required (SGAI depends on it) -->
<script src="//cdn.bitmovin.com/player/web/8/modules/bitmovinplayer-advertising-core.js"></script>
<!-- SGAI / HLS Interstitials — required -->
<script src="//cdn.bitmovin.com/player/web/8/modules/bitmovinplayer-sgai.js"></script>
<!-- Style — only required for viewability tracking -->
<script src="//cdn.bitmovin.com/player/web/8/modules/bitmovinplayer-style.js"></script>
bitmovin.player.Player.addModule(bitmovin.player['advertising-core'].default);
bitmovin.player.Player.addModule(bitmovin.player['sgai'].default);
// Only when using viewability tracking:
bitmovin.player.Player.addModule(bitmovin.player['style'].default);

Basic setup

Interstitials handling is off by default. Enable it with the tweaks.enable_sgai_handling tweak (requires the Advertising Core module, available since v8.238.0). Once enabled and the module is loaded, the player picks up interstitials from the manifest automatically.

const playerConfig = {
  key: '<PLAYER_LICENSE_KEY>',
  tweaks: {
    // Enables HLS Interstitials / SGAI handling. Off by default.
    enable_sgai_handling: true,
  },
};

const player = new bitmovin.player.Player(
  document.getElementById('player-container'),
  playerConfig,
);

const source = {
  hls: 'https://your-cdn.example.com/stream/master.m3u8',
};

player.load(source);
📘

Safari

Safari plays HLS with its native player. To enable interstitials handling there, also set the tweaks.native_hls_parsing tweak so the player fetches and parses the HLS playlist itself.

tweaks: {
  enable_sgai_handling: true,
  native_hls_parsing: true,
}

Optional behavior is configured under hls.interstitials in the player config. See the InterstitialsConfig API reference for the full list of options.

const playerConfig = {
  key: '<PLAYER_LICENSE_KEY>',
  hls: {
    interstitials: {
      // callbacks, tracking mapping, and viewability options go here
    },
  },
};

Controlling which interstitials load and play

SGAI exposes callbacks so you can filter interstitials on the client. Each is optional; when omitted, all signaled interstitials load and play.

Skip loading an interstitial's asset list

const playerConfig = {
  key: '<PLAYER_LICENSE_KEY>',
  hls: {
    interstitials: {
      shouldLoadInterstitial: interstitial => {
        // Only load the pre-roll; skip everything else before any network request
        return interstitial.id === 'pre-roll-1';
      },
    },
  },
};

Skip playback of an interstitial that was already loaded

Return false to skip playback of an interstitial; true (or omitting the callback) plays it.

hls: {
  interstitials: {
    // Skip the mid-roll; play every other interstitial
    shouldPlayInterstitial: interstitial => interstitial.id !== 'mid-roll-1',
  },
}

Choose which jumped-over interstitials still play

When a user seeks across several interstitials, decide which ones must still be played:

hls: {
  interstitials: {
    shouldPlayJumpedOverInterstitials: (interstitials, fromTime, toTime) => {
      // Only play interstitials longer than 15 seconds
      return interstitials.filter(interstitial => interstitial.duration > 15);
    },
  },
}

Playback restrictions

When an interstitial signals an X-RESTRICT attribute (see the SGAI overview for the manifest format), the Web Player enforces it and exposes the mapped values through these enums:

X-RESTRICT valueEffectEnum
SKIPSeeking / higher playback rates disabled during the adSeekingRestriction.NO_SEEKING
JUMPAd break can't be jumped over — at least one ad must playJumpPolicy.RESTRICT

The SGAI API

Access SGAI-specific controls through player.sgai. It is null when the SGAI module isn't loaded, so guard access:

const sgai = player.sgai;
if (sgai) {
  // All scheduled SGAI ads (does NOT include the currently active ad)
  const scheduled = sgai.list();

  // The currently playing SGAI ad, if any
  const active = sgai.getActiveAd();

  // Skip the currently playing ad
  await sgai.skip();
}
MethodReturnsDescription
list()SgaiAd[] | HlsInterstitial[]All scheduled SGAI ads, excluding the active one.
getActiveAd()LinearAd | undefinedThe currently active SGAI ad, if one is playing.
skip()Promise<void>Skips the currently playing ad.

Events

SGAI reuses the standard advertising events, so existing ad handling works without change. The player fires:

  • AdBreakStarted / AdBreakFinished — around each interstitial ad break
  • AdStarted / AdFinished — around each individual ad
  • AdSkipped — when an ad is skipped (e.g. via player.sgai.skip())
  • AdError — when an interstitial fails (see Error handling)
player.on(bitmovin.player.PlayerEvent.AdBreakStarted, event => {
  console.log('SGAI ad break started', event.adBreak);
});

player.on(bitmovin.player.PlayerEvent.AdStarted, event => {
  console.log('SGAI ad started', event.ad);
});

player.on(bitmovin.player.PlayerEvent.AdFinished, () => {
  console.log('SGAI ad finished');
});

For SGAI, the adBreak on these events is an SgaiAdBreak (extends AdBreak) and carries extra fields such as totalPlannedDuration, restrictions, and adCreativeSignaling. Individual ads are SgaiLinearAd (extends LinearAd) with adCreativeSignaling and customAdTrackingInfo.

Ad tracking and creative signaling

SGAI supports server-driven tracking via custom attributes on the interstitial tags (e.g. SVTA X-AD-CREATIVE-SIGNALING). You map those attributes into tracking events the player fires at the right moments.

Use a built-in preset

The simplest path is the standard SVTA mapping preset:

hls: {
  interstitials: {
    // 'AD-CREATIVE-SIGNALING' is the value of CustomAttributesMappingPresets.AD_CREATIVE_SIGNALING
    customAttributesMappingPreset: 'AD-CREATIVE-SIGNALING',
  },
}

Custom mapping callback

For full control, provide customAttributesMapping. It receives the parsed tracking data and a registry to register tracking events and the click-through URL:

hls: {
  interstitials: {
    customAttributesMapping(mappingData, mappingRegistry) {
      const adCreativeSignaling = mappingData.customAttributes['X-AD-CREATIVE-SIGNALING'];
      const payload = adCreativeSignaling && adCreativeSignaling.payload;
      if (!payload) {
        return;
      }

      const first = payload[0];
      if (first.clickThrough) {
        mappingRegistry.clickThroughUrl = first.clickThrough;
      }

      for (const trackingEvent of first.tracking || []) {
        const { type, urls, offset } = trackingEvent;
        if (!type || !urls) {
          continue;
        }
        // 'impression' is the value of AdTrackingEventTrigger.IMPRESSION
        if (type === 'impression') {
          mappingRegistry.tracking.register('impression', { urls, offset });
        }
      }
    },
  },
}
📘

Precedence

When customAttributesMapping is set, customAttributesMappingPreset is ignored.

The player fires registered tracking URLs for the full ad lifecycle, including impression, start, firstQuartile, midpoint, thirdQuartile, complete, pause, resume, mute, unmute, clickTracking, skip, and progress triggers (see AdTrackingEventTrigger).

Viewability tracking

The player can emit viewable / notViewable tracking events based on how long the ad stays visible. Viewability tracking requires two things:

  1. Player visibility tracking enabled via style.visibility.enableTracking (off by default). With the modular player, the Style module (bitmovinplayer-style.js) must also be loaded.
  2. The hls.interstitials.viewability options to tune the decision.
const playerConfig = {
  key: '<PLAYER_LICENSE_KEY>',
  style: {
    visibility: {
      // Required — enables player viewport visibility tracking
      enableTracking: true,
      // Minimum visible percentage (0–100) to count the player as visible. Default: 50
      visibilityThreshold: 50,
    },
  },
  hls: {
    interstitials: {
      viewability: {
        // Consecutive seconds visible before emitting `viewable` (default: 2)
        viewableDuration: 2,
        // Max window to make the viewability decision (default: ad duration)
        decisionTimeout: 10,
      },
    },
  },
};

If no viewable decision is reached within decisionTimeout, the tracker emits notViewable. When viewability can't be determined for technical reasons, viewUndetermined is emitted.

Error handling

SGAI failures surface as an AdError event whose top-level ErrorCode is MODULE_SGAI_ERROR (3101). The AdvertisingError.code on that event carries a more specific SgaiErrorCode:

SgaiErrorCodeValueMeaning
INTERSTITIAL_ASSET_LIST_INVALID111Asset list downloaded but malformed / missing a valid ASSETS array. (An empty no-fill response is a warning, not an error.)
INTERSTITIAL_ASSET_LIST_LOAD_FAILED406Asset list couldn't be downloaded (network error or non-2xx response).
INTERSTITIAL_ASSET_PLAYBACK_FAILED407An ad asset failed to play (network/decode error). Underlying cause is in AdvertisingError.deficiencyData.
player.on(bitmovin.player.PlayerEvent.AdError, event => {
  if (event.code === bitmovin.player.ErrorCode.MODULE_SGAI_ERROR) {
    console.error('SGAI error', event.data && event.data.code, event.name);
    // event.data.deficiencyData holds the underlying playback error, when present
  }
});

What's next