Skip to main content
Version: 11.13.0

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

Generated code
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

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.

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, Google IMA and Subscribe to ad events.

Code
demo.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>