# Advertisement tester

Schedule ad breaks in a stream and inspect the resulting ad events and companion ads.

Tip: open your browser's developer console and use the `player` and `THEOplayer` variables to interact with this demo.

### Ad configuration

Ad templatesVAST pre-roll (vast-preroll)Video URLhttps\://cdn.theoplayer.com/video/big\_buck\_bunny/big\_buck\_bunny\_metadata.m3u8Ad URLhttps\://cdn.theoplayer.com/demos/preroll.xmlIntegrationTHEOplayer (CSAI) (csai)PositionPre-roll (start)

Load sourceReset to template

Generated code

```js
player.source = {
  "sources": {
    "src": "https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny_metadata.m3u8",
    "type": "application/x-mpegurl"
  },
  "poster": "https://cdn.theoplayer.com/video/big_buck_bunny/poster.jpg",
  "metadata": {
    "title": "Big Buck Bunny"
  },
  "ads": [
    {
      "sources": "https://cdn.theoplayer.com/demos/preroll.xml",
      "integration": "csai",
      "timeOffset": "start"
    }
  ]
};

// Schedule an ad break at any time:
player.ads.schedule({
  "sources": "https://cdn.theoplayer.com/demos/preroll.xml",
  "integration": "csai"
});
```

### Events

Clear

No events reported yet.

THEOplayer supports client-side ad insertion (CSAI) of VAST and VMAP ads through the `csai` integration, and ads served through Google's IMA SDK with the `google-ima` integration. VPAID and non-linear overlay ads require `google-ima`. Ad breaks are scheduled with the `ads` array in the source, or at any time with `player.ads.schedule()`. A `timeOffset` of `'start'` schedules a pre-roll, `'end'` a post-roll, and a timestamp or percentage a mid-roll; VMAP ads carry their own offsets and ignore `timeOffset`.

```js
player.source = {
  sources: { src: 'https://example.com/video.m3u8' },
  ads: [
    {
      sources: 'https://example.com/vast.xml',
      integration: 'csai',
      timeOffset: 'start',
    },
  ],
};

// Schedule an ad break while the content is already playing:
player.ads.schedule({
  sources: 'https://example.com/vast.xml',
  integration: 'csai',
});
```

See [Set up VAST and VMAP](https://optiview.dolby.com/docs/theoplayer/how-to-guides/web/ads/set-up-vast-and-vmap.md), [Google IMA](https://optiview.dolby.com/docs/theoplayer/how-to-guides/web/ads/google-ima.md) and [Subscribe to ad events](https://optiview.dolby.com/docs/theoplayer/how-to-guides/web/ads/subscribe-to-ad-events.md).

Code

demo.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Advertisement tester</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet" />
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
      }

      theoplayer-default-ui:not(:defined) {
        display: inline-block;
        box-sizing: border-box;
      }

      theoplayer-default-ui {
        width: 100%;
        aspect-ratio: 16 / 9;
        font-family: 'Noto Sans', sans-serif;
        background: #000;
      }
    </style>
    <script src="https://imasdk.googleapis.com/js/sdkloader/ima3.js"></script>
    <script src="https://cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"></script>
    <script nomodule src="https://unpkg.com/@theoplayer/web-ui@2/polyfills"></script>
    <script async src="https://unpkg.com/@theoplayer/web-ui@2"></script>
  </head>
  <body>
    <!--
      libraryLocation: For demonstration purposes, we use the THEOplayer CDN.
      For production use, host the library yourself and update this URL and the script above.

      licenseUrl: Point this to your license file, or replace it with a license property.
    -->
    <theoplayer-default-ui
      id="player-ui"
      configuration='{"libraryLocation":"https://cdn.theoplayer.com/dash/theoplayer/","licenseUrl":"../../../../theoplayer-license.txt","mutedAutoplay":"all"}'
      source='{"sources":{"src":"https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny_metadata.m3u8","type":"application/x-mpegurl"},"poster":"https://cdn.theoplayer.com/video/big_buck_bunny/poster.jpg","metadata":{"title":"Big Buck Bunny"},"ads":[{"sources":"https://cdn.theoplayer.com/demos/preroll.xml","integration":"csai","timeOffset":"start"}]}'
    ></theoplayer-default-ui>
    <script src="../utils.js"></script>
    <script>
      const adEventNames = [
        'addadbreak',
        'removeadbreak',
        'adbreakbegin',
        'adbegin',
        'adloaded',
        'adimpression',
        'adfirstquartile',
        'admidpoint',
        'adthirdquartile',
        'adskip',
        'adend',
        'adbreakend',
        'aderror',
        'adclicked',
      ];

      const allowedIntegrations = ['csai', 'google-ima'];

      function isSafeUrl(value) {
        if (typeof value !== 'string' || value.length === 0) return false;
        try {
          const url = new URL(value);
          return url.protocol === 'https:' || url.protocol === 'http:' || url.protocol === 'data:';
        } catch {
          return false;
        }
      }

      function sanitizeAdDescription(ad) {
        if (typeof ad !== 'object' || ad == null || !isSafeUrl(ad.sources)) return null;
        const description = {
          sources: ad.sources,
          integration: allowedIntegrations.includes(ad.integration) ? ad.integration : 'csai',
        };
        if (typeof ad.timeOffset === 'number' && Number.isFinite(ad.timeOffset)) {
          description.timeOffset = ad.timeOffset;
        } else if (typeof ad.timeOffset === 'string' && /^(start|end|\d+%|\d{1,2}:\d{2}:\d{2}(\.\d+)?)$/.test(ad.timeOffset)) {
          description.timeOffset = ad.timeOffset;
        }

        return description;
      }

      function sanitizeSource(source) {
        if (typeof source !== 'object' || source == null) return null;
        const src = source.sources && source.sources.src;
        if (!isSafeUrl(src)) return null;
        const allowedTypes = ['application/dash+xml', 'application/x-mpegurl'];
        const result = {
          sources: allowedTypes.includes(source.sources.type) ? { src, type: source.sources.type } : { src },
        };
        if (isSafeUrl(source.poster)) result.poster = source.poster;
        if (source.metadata && typeof source.metadata.title === 'string') result.metadata = { title: source.metadata.title };
        if (Array.isArray(source.ads)) {
          const ads = source.ads.map(sanitizeAdDescription).filter(Boolean);
          if (ads.length > 0) result.ads = ads;
        }
        return result;
      }

      onPlayerReady((player) => {
        let events = [];

        function reportEvents() {
          sendToParent('ad-events', { events: events.slice() });
        }

        function logEvent(name, details) {
          events = [
            {
              time: new Date().toLocaleTimeString('en-GB', { hour12: false }),
              event: name,
              details: details || '',
            },
            ...events,
          ].slice(0, 100);
          reportEvents();
        }

        function describeEvent(name, event) {
          const details = [];
          if (event.ad) {
            if (event.ad.id) details.push(`ad ${event.ad.id}`);
            if (event.ad.integration) details.push(event.ad.integration);
            if (typeof event.ad.duration === 'number' && Number.isFinite(event.ad.duration)) {
              details.push(`duration ${event.ad.duration.toFixed(1)} s`);
            }
          }
          if (event.adBreak && typeof event.adBreak.timeOffset === 'number') {
            details.push(`break at ${event.adBreak.timeOffset.toFixed(1)} s`);
          }
          const errorMessage = event.error?.message || event.error || event.message;
          if (errorMessage) details.push(String(errorMessage));
          logEvent(name, details.join(' · '));
        }

        function reportCompanions(companions) {
          sendToParent('companions', {
            companions: companions.map((companion) => ({
              resourceURI: typeof companion.resourceURI === 'string' ? companion.resourceURI : '',
              clickThrough: typeof companion.clickThrough === 'string' ? companion.clickThrough : '',
              altText: typeof companion.altText === 'string' ? companion.altText : '',
            })),
          });
        }

        player.ads.addEventListener(adEventNames, (event) => {
          describeEvent(event.type, event);
          if (event.type === 'adbegin' && event.ad && Array.isArray(event.ad.companions) && event.ad.companions.length > 0) {
            reportCompanions(event.ad.companions);
          }
          if (event.type === 'adbreakend') {
            reportCompanions([]);
          }
        });
        player.addEventListener('sourcechange', (event) => {
          reportCompanions([]);
          describeEvent('sourcechange', event);
        });
        player.addEventListener('error', () => {
          const error = player.errorObject;
          logEvent('error', error?.message || 'Player error');
        });

        onParentMessage('load-ad-source', (message) => {
          const source = sanitizeSource(message.source);
          if (!source) return;
          player.autoplay = true;
          player.source = source;
        });
        onParentMessage('clear-ad-log', () => {
          events = [];
          reportEvents();
        });
      });
    </script>
  </body>
</html>
```
